0. 概述
其中,Dropout 与权重衰减 (Weight Decay) 两种方法已经在以后实验中(过拟合专题)练习,本次实验我们将练习其他一些重要的网络优化方法。
具体包括:
(1) 小批量梯度下降(这部分将讨论 batch size 与学习率的设置问题)
(2) 学习率衰减 (Learning Rate Decay)
(3) 批量归一化 (Batch Normalization)
(4) 数据预处理
1. PyTorch 库导入及数据集介绍
import torch import torchvision import torch.nn as nn import torch.nn.functional as F import torch.optim as optim print(torch.manual_seed(1))
<torch._C.Generator object at 0x000001C4DFC45B50>
关于数据集:
本次实验我们将用到一个新的图片数据集:CIFAR-10。该数据集共有 60000 张 RGB 彩色图像,其中 50000 张用于训练,10000 张用于测试。图片共分为 10 个类别,每类各有 6000 张图片,每张图片包含 32*32 个像素点。
数据集样例和 10 个类别如下所示:
CIFAR-10 的官方说明及下载地址:http://www.cs.toronto.edu/~kriz/cifar.html
相较于之前用过的 MNIST 和 Fashion-MNIST 来说,CIFAR-10 的分类难度显著增加。
2. 对照组结果
# 训练集 batch_size_small = 10 # 小批次:每次处理 10 张图片 train_set = torchvision.datasets.CIFAR10('./dataset_cifar10', train=True, download=True, transform=torchvision.transforms.Compose([ torchvision.transforms.ToTensor(), torchvision.transforms.Normalize( (0.4914,0.4822,0.4465), (0.2023,0.1994,0.2010) ) ]) ) # 测试集 test_set = torchvision.datasets.CIFAR10('./dataset_cifar10', train=False, download=True, transform=torchvision.transforms.Compose([ torchvision.transforms.ToTensor(), torchvision.transforms.Normalize( (0.4914,0.4822,0.4465), (0.2023,0.1994,0.2010) ) ])) train_loader_small = torch.utils.data.DataLoader(train_set, batch_size=batch_size_small, shuffle=True) test_loader_small = torch.utils.data.DataLoader(test_set, batch_size=batch_size_small, shuffle=True)
Files already downloaded and verified Files already downloaded and verified
设计一个普通的卷积神经网络,其中包含三个卷积层和两个全连接层。
class CNN5_SmallBatch(nn.Module): def __init__(self): super(CNN5_SmallBatch, self).__init__() self.conv1 = nn.Conv2d(in_channels=3, out_channels=32, padding=1, kernel_size=3) self.conv2 = nn.Conv2d(in_channels=32, out_channels=32, padding=1, kernel_size=3) self.conv3 = nn.Conv2d(in_channels=32, out_channels=32, padding=1, kernel_size=3) self.fc1 = nn.Linear(in_features=32*4*4, out_features=64) self.out = nn.Linear(in_features=64, out_features=10) def forward(self, t): t = self.conv1(t) t = F.relu(t) t = F.max_pool2d(t, kernel_size=2, stride=2) t = self.conv2(t) t = F.relu(t) t = F.max_pool2d(t, kernel_size=2, stride=2) t = self.conv3(t) t = F.relu(t) t = F.max_pool2d(t, kernel_size=2, stride=2) t = t.reshape(batch_size_small, 32*4*4) # dim0: batch size = 10 t = self.fc1(t) t = F.relu(t) t = self.out(t) return t
network = CNN5_SmallBatch() print(network.cuda()) 1 2 CNN5_SmallBatch( (conv1): Conv2d(3, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (conv2): Conv2d(32, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (conv3): Conv2d(32, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (fc1): Linear(in_features=512, out_features=64, bias=True) (out): Linear(in_features=64, out_features=10, bias=True) )
训练前准备
loss_func = nn.CrossEntropyLoss() # 损失函数 optimizer = optim.SGD(network.parameters(), lr=0.1) # 优化器(学习率 lr 等于 0.1)
def get_num_correct(preds, labels): # get the number of correct times return preds.argmax(dim=1).eq(labels).sum().item()
开始训练
total_epochs = 10 # 训练 10 个周期 for epoch in range(total_epochs): total_loss = 0 total_train_correct = 0 for batch in train_loader_small: images, labels = batch images = images.cuda() labels = labels.cuda() preds = network(images) loss = loss_func(preds, labels) optimizer.zero_grad() loss.backward() optimizer.step() total_loss += loss.item() total_train_correct += get_num_correct(preds, labels) print("epoch:", epoch, "correct times:", total_train_correct, f"training accuracy:", "%.3f" %(total_train_correct/len(train_set)*100), "%", "total_loss:", "%.3f" %total_loss) torch.save(network.cpu(), "cnn5.pt") # 保存模型
epoch: 0 correct times: 20931 training accuracy: 41.862 % total_loss: 8056.230 epoch: 1 correct times: 24131 training accuracy: 48.262 % total_loss: 7359.204 epoch: 2 correct times: 24193 training accuracy: 48.386 % total_loss: 7401.946 epoch: 3 correct times: 24470 training accuracy: 48.940 % total_loss: 7411.769 epoch: 4 correct times: 24225 training accuracy: 48.450 % total_loss: 7540.996 epoch: 5 correct times: 23992 training accuracy: 47.984 % total_loss: 7580.270 epoch: 6 correct times: 24355 training accuracy: 48.710 % total_loss: 7503.453 epoch: 7 correct times: 24148 training accuracy: 48.296 % total_loss: 7539.350 epoch: 8 correct times: 23923 training accuracy: 47.846 % total_loss: 7676.186 epoch: 9 correct times: 23948 training accuracy: 47.896 % total_loss: 7615.532
查看测试结果
network = CNN5_SmallBatch() # 加载模型 network = torch.load("cnn5.pt") total_test_correct = 0 total_loss = 0 for batch in test_loader_small: images, labels = batch images = images labels = labels preds = network(images) loss = loss_func(preds, labels) total_loss += loss total_test_correct += get_num_correct(preds, labels) print("correct times:", total_test_correct, f"test accuracy:", "%.3f" %(total_test_correct/len(test_set)*100), "%", "total_loss:", "%.3f" %total_loss)
correct times: 4475 test accuracy: 44.750 % total_loss: 1604.812
3. 小批量梯度下降:batch size 与学习率 (lr) 设置
在理论课中,我们已经学习了什么是小批量梯度下降,下面我们来对如下结论予以验证:
批量越大,随机梯度的方差越小,引入的噪声也越小,训练也越稳定,因此可以设置较大的学习率;
批量较小时,需要设置较小的学习率,否则模型会不收敛。
(1) 为了验证结论,我们将 batch size 从原先的 10 增加到 100,维持学习率 0.1 不变。观察此时训练和测试结果并讨论。
训练结果,测试结果如图1,图2所示。
图 1训练结果(batch size=100,lr=0.1)
图 2 测试结果(batch size=100,lr=0.1)
与对照组对比发现:网络测试准确率从42%左右提高到了71%左右,有了较大提升。原因在于:保持相同的学习率保证了优化算法每次迈出的步子大小相同;增大batch size优化算法可以获得更多的损失信息,这样它可以朝着更加正确的方向(全局或局部最小值)迈步子。有了相同大小的步子而方向更加准确,因此在有限的训练周期内会获得更好的训练效果,测试效果自然更好。因此大的batch size,对应大的学习率。
4. 学习率衰减策略(Learning Rate Decay)
一般来说,我们希望在训练初期学习率大一些,使得网络收敛迅速,在训练后期学习率小一些,使得网络能够更好的收敛到最优解。
下面我们来做如下测试:保持 batch size 为 10,每个周期结束后,使得学习率衰减为原先的 0.8 倍。观察此时训练和测试结果并讨论。
训练结果如图3所示,测试结果如图4所示。
图3 训练结果(学习率衰减)
图4 测试结果(学习率衰减)
与对照组对比发现:网络测试准确率从42%左右提高到了71%左右,有了较大提升。原因在于:在学习初期通常处于离最小值较远的地方,因此采用较大的学习率迈出较大的步子加快训练速度。但是随着训练的进行,已经很靠近最小值了,这时要学习率小一些,使得网络能够更好的收敛到最优解。
除了手动减少学习率,PyTorch 也为我们提供了多种学习率衰减策略,感兴趣的同学可以阅读官网上的说明 “How to adjust learning rate” (https://pytorch.org/docs/stable/optim.html) 。
5. 批量归一化 (Batch Normalization)
批量归一化方法目前已经被广泛的应用在神经网络中,它有很多显著的优点,例如能够有效提高网络泛化能力、可以选择比较大的初始学习率、能够提高网络收敛性等。
感兴趣的同学可参考文献:《Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift》
下面我们维持 batch size (10) 和学习率 (0.1) 不变,重新定义并训练一个包含 batch normalization layer (nn.BatchNorm2d) 的卷积神经网络。请同学们观察训练和测试结果并讨论。
# ------------- data loader (batch size = 10) ------------- # 使用 train_loader_small 和 test_loader_small # ------------- network (包含批量归一化处理) ------------- class CNN5_BatchNorm(nn.Module): def __init__(self): super(CNN5_BatchNorm, self).__init__() self.conv1 = nn.Conv2d(in_channels=3, out_channels=32, padding=1, kernel_size=3) self.bn1 = nn.BatchNorm2d(32) # 批量归一化层 (32:输入通道数) self.conv2 = nn.Conv2d(in_channels=32, out_channels=32, padding=1, kernel_size=3) self.bn2 = nn.BatchNorm2d(32) # 批量归一化层 (32:输入通道数) self.conv3 = nn.Conv2d(in_channels=32, out_channels=32, padding=1, kernel_size=3) self.bn3 = nn.BatchNorm2d(32) # 批量归一化层 (32:输入通道数) self.fc1 = nn.Linear(in_features=32*4*4, out_features=64) self.out = nn.Linear(in_features=64, out_features=10) def forward(self, t): t = self.conv1(t) # conv1 t = self.bn1(t) # batch normalization t = F.relu(t) t = F.max_pool2d(t, kernel_size=2, stride=2) t = self.conv2(t) # conv2 t = self.bn2(t) # batch normalization t = F.relu(t) t = F.max_pool2d(t, kernel_size=2, stride=2) t = self.conv3(t) # conv3 t = self.bn3(t) # batch normalization t = F.relu(t) t = F.max_pool2d(t, kernel_size=2, stride=2) t = t.reshape(batch_size_small, 32*4*4) t = self.fc1(t) # fc1 t = F.relu(t) t = self.out(t) # output layer return t network = CNN5_BatchNorm() network.cuda() # ------------- 训练 ------------- optimizer = optim.SGD(network.parameters(), lr=0.1) # 学习率 lr 设置为 0.1 for epoch in range(total_epochs): total_loss = 0 total_train_correct = 0 for batch in train_loader_small: images, labels = batch images = images.cuda() labels = labels.cuda() preds = network(images) loss = loss_func(preds, labels) optimizer.zero_grad() loss.backward() optimizer.step() total_loss += loss.item() total_train_correct += get_num_correct(preds, labels) print("epoch:", epoch, "correct times:", total_train_correct, f"training accuracy:", "%.3f" %(total_train_correct/len(train_set)*100), "%", "total_loss:", "%.3f" %total_loss) torch.save(network.cpu(), "cnn5.pt") # 保存模型
训练结果如图5所示,测试结果如图6所示。
图5 训练结果(批量归一化)
图6 测试结果(批量归一化)
与对照组对比发现:网络测试准确率从42%左右提高到了73%左右,有了较大提升。原因在于:在深层神经网络中,中间层的输入是上一层神经网络的输出。因此,之前的层的神经网络参数的变化会导致当前层输入的分布发生较大的差异。在使用随机梯度下降法来训练神经网络时,每次参数更新都会导致网络中每层的输入分布发生变化。越是深层的神经网络,其输入的分布会改变的越明显。从机器学习角度来看,如果某层的输入分布发生了变化,那么其参数需要重新学习,这种现象称为内部协变量偏移。为了解决内部协变量偏移问题,就要使得每一层神经网络输入的分布在训练过程中要保持一致。最简单的方法是对每一层神经网络都进行归一化操作,使其分布保持稳定。批量归一化可以使得每一层神经网络输入的分布在训练过程中要保持一致,因此可以有效提高网络性能。
6. 数据预处理
在之前的实验中,我们在将数据集传递给神经网络前,每次都对数据进行了标准化处理 (data normalization)。下面我们取消标准化和 shuffle 设置,将 batch size 和学习率分别设为 100 和 0.1,请和 3-(1) 对比,观察数据预处理是否对结果有所影响。
# ------------- data loader (batch size = 100,取消标准化处理) ------------- train_set_noTransform = torchvision.datasets.CIFAR10('./dataset_cifar10', train=True, download=True, transform=torchvision.transforms.Compose([ torchvision.transforms.ToTensor(), ])) test_set_noTransform = torchvision.datasets.CIFAR10('./dataset_cifar10', train=False, download=True, transform=torchvision.transforms.Compose([ torchvision.transforms.ToTensor(), ]))
训练结果如图7所示,测试结果如图8所示。
图7 训练结果(batch size=100,lr=0.1,无预处理)
图8 测试结果(batch size=100,lr=0.1,无预处理)
与“batch size=100,lr=0.1,有预处理”的组别对比发现:网络测试准确率从71%左右降低到了65%左右,有明显下降。分析原因:加入预处理可以使输入的数据处在一个更好的分布更有利于网络的训练,使得优化算法更容易找到最优解方向,收敛更快。