虽然 AlexNet 证明深层神经网络卓有成效,但由于它的结构 “混乱”,无法提供一个通用的模板来指导后续的研究人员设计新的网络。
为了使神经网络的结构看起来更加规整,我们可以将若干卷积层和一个汇聚层封装成一个块,再将若干个块与全连接层连接起来以形成一个完整的网络。
使用块的想法首先出现在牛津大学的视觉几何组(Visual Geometry Group)中的 VGG 网络中。通过使用循环和子程序,我们可以很容易地在任何现代深度学习框架中实现这些重复的架构。
经典CNN的基本组成部分是如下这样一个序列:
而一个VGG块与之类似,由若干个卷积层和一个最大汇聚层组成。在最初的VGG论文中,作者使用了带有 3 × 3 3\times3 3×3 卷积核、填充为 1 1 1(保持高度和宽度)的卷积层,和带有 2 × 2 2\times2 2×2 汇聚窗口、步幅为 2 2 2(每个块后的分辨率减半)的最大汇聚层。
AlexNet 和 VGG 的架构比较如下图:
为了实现 VGG 网络,我们首先需要构造 VGG 块。一个 VGG 块应包含三个参数:输入通道数、输出通道数和卷积层的个数。
很显然,因为 VGG 包含了多个卷积层,所以只有第一个卷积层的输入通道数与输出通道数不同,剩余卷积层的输入通道数与输出通道数保持一致。
代码实现如下:
class VGGBlock(nn.Module):
def __init__(self, in_channels, out_channels, num_convs):
super().__init__()
self.conv = nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1),
nn.ReLU(),
)
for _ in range(num_convs - 1):
self.conv.append(nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1))
self.conv.append(nn.ReLU())
self.pool = nn.MaxPool2d(kernel_size=2, stride=2)
def forward(self, x):
x = self.conv(x)
x = self.pool(x)
return x
尝试创建一个 VGG 块实例并打印结构:
vgg_block = VGGBlock(3, 5, 3)
print(vgg_block)
# VGGBlock(
# (conv): Sequential(
# (0): Conv2d(3, 5, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
# (1): ReLU()
# (2): Conv2d(5, 5, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
# (3): ReLU()
# (4): Conv2d(5, 5, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
# (5): ReLU()
# )
# (pool): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
# )
接下来使用 VGG 块构造 VGG 网络。VGG 网络初始化应传入的参数为由元组构成的可迭代对象,这里假设是列表。其中每个元组的第一个参数代表当前 VGG 块中输入通道个数,第二个参数代表输出通道个数,第三个参数代表卷积层的个数。
注意 VGG 网络也是针对 224 × 224 224\times 224 224×224 分辨率的图像。
class VGG(nn.Module):
def __init__(self, vgg_arch):
super().__init__()
self.conv = nn.Sequential()
for in_channels, out_channels, num_convs in vgg_arch:
self.conv.append(VGGBlock(in_channels, out_channels, num_convs))
self.fc = nn.Sequential(
nn.Linear(out_channels * 7 * 7, 4096), nn.ReLU(),
nn.Dropout(0.5),
nn.Linear(4096, 4096), nn.ReLU(),
nn.Dropout(0.5),
nn.Linear(4096, 10),
)
def forward(self, x):
x = self.conv(x)
x = torch.flatten(x, 1)
x = self.fc(x)
return x
尝试创建一个 VGG 网络实例并打印结构:
vgg_arch = [(3, 64, 1), (64, 128, 1)]
vgg = VGG(vgg_arch)
print(vgg)
# VGG(
# (conv): Sequential(
# (0): VGGBlock(
# (conv): Sequential(
# (0): Conv2d(3, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
# (1): ReLU()
# )
# (pool): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
# )
# (1): VGGBlock(
# (conv): Sequential(
# (0): Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
# (1): ReLU()
# )
# (pool): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
# )
# )
# (fc): Sequential(
# (0): Linear(in_features=6272, out_features=4096, bias=True)
# (1): ReLU()
# (2): Dropout(p=0.5, inplace=False)
# (3): Linear(in_features=4096, out_features=4096, bias=True)
# (4): ReLU()
# (5): Dropout(p=0.5, inplace=False)
# (6): Linear(in_features=4096, out_features=10, bias=True)
# )
# )
考虑这样一个 VGG 网络,它有5个卷积块,其中前两个块各有一个卷积层,后三个块各包含两个卷积层。 第一个模块有64个输出通道,每个后续模块将输出通道数量翻倍,直到该数字达到512。由于该网络使用8个卷积层和3个全连接层,因此它通常被称为VGG-11。
前面已经实现了 VGG 类,于是很容易构造 VGG-11
# 假设输入图片的通道数是1
vgg_arch = [(1, 64, 1), (64, 128, 1), (128, 256, 2), (256, 512, 2), (512, 512, 2)]
vgg = VGG(vgg_arch)
我们使用 Fashion-MNIST 数据集来检验 VGG 的性能
transformer = torchvision.transforms.Compose([Resize(224), ToTensor()])
train_data = torchvision.datasets.FashionMNIST('/mnt/mydataset', train=True, transform=transformer, download=True)
test_data = torchvision.datasets.FashionMNIST('/mnt/mydataset', train=False, transform=transformer, download=True)
train_loader = DataLoader(train_data, batch_size=64, shuffle=True, num_workers=4)
test_loader = DataLoader(test_data, batch_size=64, num_workers=4)
由于 VGG-11 比 AlexNet 计算量更大,为节省时间,这里构造一个减弱版的 VGG-11,设置学习率为 0.05,训练 20 个 Epoch
vgg = VGG([(1, 16, 1), (16, 32, 1), (32, 64, 2), (64, 128, 2), (128, 128, 2)])
e = E(train_loader, test_loader, vgg, 20, 0.05)
e.main()
e.show()
在 NVIDIA GeForce RTX 3090 上的训练结果如下所示(这里仅展示最后一个Epoch以及整体的变化图):
Epoch 20
--------------------------------------------------
Train Avg Loss: 0.044154, Train Accuracy: 0.983633
Test Avg Loss: 0.387796, Test Accuracy: 0.914200
--------------------------------------------------
5410.8 samples/sec
--------------------------------------------------
Done!
import torch
import torchvision
from torch import nn
from torch.utils.data import DataLoader
from torchvision.transforms import ToTensor, Resize
from Experiment import Experiment as E
class VGGBlock(nn.Module):
def __init__(self, in_channels, out_channels, num_convs):
super().__init__()
self.conv = nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1),
nn.ReLU(),
)
for _ in range(num_convs - 1):
self.conv.append(nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1))
self.conv.append(nn.ReLU())
self.pool = nn.MaxPool2d(kernel_size=2, stride=2)
def forward(self, x):
x = self.conv(x)
x = self.pool(x)
return x
class VGG(nn.Module):
def __init__(self, vgg_arch):
super().__init__()
self.conv = nn.Sequential()
for in_channels, out_channels, num_convs in vgg_arch:
self.conv.append(VGGBlock(in_channels, out_channels, num_convs))
self.fc = nn.Sequential(
nn.Linear(out_channels * 7 * 7, 4096), nn.ReLU(),
nn.Dropout(0.5),
nn.Linear(4096, 4096), nn.ReLU(),
nn.Dropout(0.5),
nn.Linear(4096, 10),
)
def forward(self, x):
x = self.conv(x)
x = torch.flatten(x, 1)
x = self.fc(x)
return x
transformer = torchvision.transforms.Compose([Resize(224), ToTensor()])
train_data = torchvision.datasets.FashionMNIST('/mnt/mydataset', train=True, transform=transformer, download=True)
test_data = torchvision.datasets.FashionMNIST('/mnt/mydataset', train=False, transform=transformer, download=True)
train_loader = DataLoader(train_data, batch_size=64, shuffle=True, num_workers=4)
test_loader = DataLoader(test_data, batch_size=64, num_workers=4)
vgg = VGG([(1, 16, 1), (16, 32, 1), (32, 64, 2), (64, 128, 2), (128, 128, 2)])
e = E(train_loader, test_loader, vgg, 20, 0.05)
e.main()
e.show()
LeNet、AlexNet 和 VGG 都有一个共同的设计模式:通过一系列的卷积层与汇聚层来提取空间结构特征,然后通过全连接层对特征的表征进行处理。 AlexNet 和 VGG 对 LeNet 的改进主要在于如何扩大和加深这两个模块,而 NiN 的想法是在每个像素位置(针对每个高度和宽度)应用一个全连接层。 如果我们将权重连接到每个空间位置,我们可以将其视为一个 1 × 1 1\times1 1×1卷积层,或作为在每个像素位置上独立作用的全连接层。从另一个角度看,即将空间维度中的每个像素视为单个样本,将通道维度视为不同特征。
与 VGG 类似,NiN 网络也有一系列的 NiN 块构成。NiN 块从一个普通的卷积层开始,后面接了两个 1 × 1 1\times1 1×1 的卷积层, 第一层的卷积窗口形状通常由用户设置。
下图展示了 VGG 与 NiN 架构的差异:
先从 NiN 块开始,代码实现如下:
class NiNBlock(nn.Module):
def __init__(self, in_channels, out_channels, kernel_size, stride, padding):
super().__init__()
self.block = nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernel_size, stride, padding), nn.ReLU(),
nn.Conv2d(out_channels, out_channels, kernel_size=1), nn.ReLU(),
nn.Conv2d(out_channels, out_channels, kernel_size=1), nn.ReLU(),
)
def forward(self, x):
return self.block(x)
最初的 NiN 网络是在 AlexNet 后不久提出的,显然从中得到了一些启示。 NiN 使用窗口形状为 11 × 11 11\times11 11×11、 5 × 5 5\times5 5×5 和 3 × 3 3\times3 3×3 的卷积层,输出通道数量与 AlexNet 中的相同。 每个 NiN 块后有一个最大汇聚层,汇聚窗口形状为 3 × 3 3\times 3 3×3,步幅为2。
NiN 和 AlexNet 之间的一个显著区别是 NiN 完全取消了全连接层。 相反,NiN 使用一个 NiN 块,其输出通道数等于标签类别的数量。最后放一个全局平均汇聚层(Global Average Pooling)。这样设计的一个优点是,它显著减少了模型参数的数量。然而在实践中,这种设计有时会增加训练模型的时间。
class NiN(nn.Module):
def __init__(self):
super().__init__()
self.features = nn.Sequential(
NiNBlock(1, 96, kernel_size=11, stride=4, padding=0),
nn.MaxPool2d(3, stride=2),
NiNBlock(96, 256, kernel_size=5, stride=1, padding=2),
nn.MaxPool2d(3, stride=2),
NiNBlock(256, 384, kernel_size=3, stride=1, padding=1),
nn.MaxPool2d(3, stride=2),
nn.Dropout(0.5),
NiNBlock(384, 10, kernel_size=3, stride=1, padding=1),
)
self.gap = nn.Sequential(
nn.AdaptiveAvgPool2d((1, 1)),
nn.Flatten(),
)
def forward(self, x):
x = self.features(x)
x = self.gap(x)
return x
我们依然使用 FashionMNIST 数据集,设置 batch size 为 64 64 64,学习率为 0.13 0.13 0.13,使用 Xavier 初始化,在 NVIDIA GeForce RTX 3090 上训练 20 个 Epoch 的结果如下(只展示最后一个Epoch和整体变化图):
Epoch 20
--------------------------------------------------
Train Avg Loss: 0.228536, Train Accuracy: 0.916433
Test Avg Loss: 0.295190, Test Accuracy: 0.894600
--------------------------------------------------
4227.0 samples/sec
--------------------------------------------------
Done!
import torch
import torchvision
from torch import nn
from torch.utils.data import DataLoader
from torchvision.transforms import ToTensor, Resize
from Experiment import Experiment as E
class NiNBlock(nn.Module):
def __init__(self, in_channels, out_channels, kernel_size, stride, padding):
super().__init__()
self.block = nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernel_size, stride, padding), nn.ReLU(),
nn.Conv2d(out_channels, out_channels, kernel_size=1), nn.ReLU(),
nn.Conv2d(out_channels, out_channels, kernel_size=1), nn.ReLU(),
)
def forward(self, x):
return self.block(x)
class NiN(nn.Module):
def __init__(self):
super().__init__()
self.features = nn.Sequential(
NiNBlock(1, 96, kernel_size=11, stride=4, padding=0),
nn.MaxPool2d(3, stride=2),
NiNBlock(96, 256, kernel_size=5, stride=1, padding=2),
nn.MaxPool2d(3, stride=2),
NiNBlock(256, 384, kernel_size=3, stride=1, padding=1),
nn.MaxPool2d(3, stride=2),
nn.Dropout(0.5),
NiNBlock(384, 10, kernel_size=3, stride=1, padding=1),
)
self.gap = nn.Sequential(
nn.AdaptiveAvgPool2d((1, 1)),
nn.Flatten(),
)
def forward(self, x):
x = self.features(x)
x = self.gap(x)
return x
transformer = torchvision.transforms.Compose([Resize(224), ToTensor()])
train_data = torchvision.datasets.FashionMNIST('/mnt/mydataset', train=True, transform=transformer, download=True)
test_data = torchvision.datasets.FashionMNIST('/mnt/mydataset', train=False, transform=transformer, download=True)
train_loader = DataLoader(train_data, batch_size=64, shuffle=True, num_workers=4)
test_loader = DataLoader(test_data, batch_size=64, num_workers=4)
def init_net(m):
if type(m) == nn.Linear or type(m) == nn.Conv2d:
nn.init.xavier_uniform_(m.weight)
nin = NiN()
nin.apply(init_net)
e = E(train_loader, test_loader, nin, 20, 0.13)
e.main()