Appearance
使用卷积进行泛化(下)
训练卷积网络
现在可以进行完整的训练循环,结构与第六章类似。在本章会增加准确率和跟踪功能,并在GPU上运行以提升训练速度,步骤如下:
- 将输入数据输入模型(前向传播)
- 计算损失(也属于前向传播的一部分)
- 清除旧的梯度
- 调用loss.backward()计算所有参数相对于损失的梯度(反向传播)
- 让优化器朝降低损失的方向迈出一步
Python
import datetime
def training_loop(n_epochs, optimizer, model, loss_fn, train_loader):
# 我们的 epoch 循环,编号从 1 到 n_epochs,而不是从 0 开始
for epoch in range(1, n_epochs + 1):
loss_train = 0.0
# 遍历数据加载器为我们创建的数据集批次
for imgs, labels in train_loader:
# 将一个批次的数据输入到我们的模型中...
outputs = model(imgs)
# ...并计算我们希望最小化的损失 (loss) loss = loss_fn(outputs, labels)
# 清除上一轮的梯度后...
optimizer.zero_grad()
# ...执行反向传播步骤。也就是说,我们计算网络中所有需要学习的参数的梯度。
loss.backward()
# 更新模型
optimizer.step()
# 对整个 epoch 中看到的损失求和。请记住,使用 .item() 将损失转换为 Python 数字以脱离梯度计算图是很重要的。
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)))使用上一章中的数据集,将封装至Dataset中,并实例化神经网络、优化器和损失函数,并调用训练循环。
Python
train_loader = torch.utils.data.DataLoader(cifar2, batch_size=64, shuffle=True)
model = Net()
optimizer = torch.optim.SGD(model.parameters(), lr=1e-2)
loss_fn = nn.CrossEntropyLoss()
training_loop(100, optimizer, model, loss_fn, train_loader)准确率评估
为了得到比损失函数更容易解释的指标,可以查看其在训练集和验证集上的准确率:
Python
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 的值会加上当前批次的大小 (batch size) total += labels.shape[0]
# 将拥有最大概率的预测类别与真实标签 (ground-truth) 进行对比,
# 首先得到一个布尔数组。通过求和 (.sum()) 即可得到该批次中预测正确的样本数量。
correct += int((predicted == labels).sum())
print("Accuracy {}: {:.2f}".format(name, correct / total))
# 调用验证函数
validate(model, train_loader, val_loader)这里有报错
Python
Traceback (most recent call last):
File "D:\Documents\Study\learn_conv.py", line 116, in <module>
validate(model, train_loader, val_loader)
~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "D:\Documents\Study\learn_conv.py", line 105, in validate
for imgs, labels in loader:
^^^^^^
File "D:\Documents\Study\.venv\Lib\site-packages\torch\utils\data\dataloader.py", line 718, in __next__
data = self._next_data()
File "D:\Documents\Study\.venv\Lib\site-packages\torch\utils\data\dataloader.py", line 778, in _next_data
data = self._dataset_fetcher.fetch(index) # may raise StopIteration
File "D:\Documents\Study\.venv\Lib\site-packages\torch\utils\data\_utils\fetch.py", line 57, in fetch
return self.collate_fn(data)
~~~~~~~~~~~~~~~^^^^^^
File "D:\Documents\Study\.venv\Lib\site-packages\torch\utils\data\_utils\collate.py", line 401, in default_collate
return collate(batch, collate_fn_map=default_collate_fn_map)
File "D:\Documents\Study\.venv\Lib\site-packages\torch\utils\data\_utils\collate.py", line 215, in collate
collate(samples, collate_fn_map=collate_fn_map)
~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "D:\Documents\Study\.venv\Lib\site-packages\torch\utils\data\_utils\collate.py", line 243, in collate
raise TypeError(default_collate_err_msg_format.format(elem_type))
TypeError: default_collate: batch must contain tensors, numpy arrays, numbers, dicts or lists; found <class 'PIL.Image.Image'>这是由于数据集传入的不是PyTorch 能识别的 Tensor,需要做如下修改:
Python
# 定义统一的图像预处理流水线(转为Tensor并进行归一化)
data_transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize(mean=(0.4914, 0.4822, 0.4465),
std=(0.2470, 0.2435, 0.2616))
])
# 实例化 CIFAR10 数据集,并应用 transform(注意:这里训练集和验证集都要加 transform)
transformed_cifar10 = datasets.CIFAR10(
root=data_path, train=True, download=True, transform=data_transform
)
transformed_cifar10_val = datasets.CIFAR10(
root=data_path, train=False, download=True, transform=data_transform
)
label_map = {0:0, 2:1}
class_names = ['airplane', 'bird']
cifar2 = [(img, label_map[label])
for img, label in transformed_cifar10
if label in [0,2]]
cifar2_val = [(img, label_map[label])
for img, label in transformed_cifar10_val
if label in [0,2]]得到Accuracy train: 0.94与Accuracy val: 0.88的训练结果。将结果转化为int类型以处理证书张量,与使用item()方法类似。该模型表现远好于全连接模型,且在验证机的准确率更高,表明卷积具有平移不变性和局部性。
保存和加载模型
我们可以将模型保存到文件中,保存的pt文件中会包含模型的所有参数,即两个卷积块与两个线性模块中的所有的权重和偏置,且不需要更新参数,在实际使用时需要载入模型:
Python
torch.save(model.state_dict(), data_path + 'birds_vs_airplans.pt')
loaded_model = Net()
loaded_model.load_state_dict(torch.load(data_path + 'birds_vs_airplans.pt'))在GPU上训练
为了加快训练速度,我们可以在GPU上进行训练:
Python
device = (torch.device('cuda') if torch.cuda.is_available() else torch.device('cpu'))
print(device)然后我们可以使用Tensor.to方法将数据从数据集加载器中获取的张量移动到GPU:
imgs, labels = imgs.to(device), labels.to(device)在训练函数中同理:
Python
train_loader = torch.utils.data.DataLoader(cifar2, batch_size=64, shuffle=True)
model = Net().to(device=device)模型设计
到目前为止已经掌握了基础的CNN图像分类,但这只是冰山一角。真实世界的数据更复杂(文本、序列、大图),需要更高级的架构(LSTM、Transformer 等)。接下来介绍的的,不是去死记硬背无数种模型,而是 PyTorch 的底层构建思维。掌握了这套内功,以后你自己去看最前沿的 AI 论文和源码时,就能游刃有余、见招拆招。
增加记忆容量:宽度
在深入探讨更复杂的内容之前,有几个维度值得升读探索,首先是网络的宽度:即每层神经元的数量或者说每个卷积的通道数。在PyTorch中只需要增大第一个卷积中指定输出的通道数,并增加后续层的大小,同时修改前向传播函数:
Python
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如果希望避免硬编码数字,可以向init中传递参数以参数化宽度:
Python
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指定每层通道数和特征数的参数直接关系到模型中的参数数量;在其他条件相同的情况下,它们会增加模型的容量。容量越大,模型能够处理的输入变化就越多;但与此同时,过拟合的可能性也更大,因为模型可以利用更多的参数来记忆输入中无关紧要的细节。