跳到主要内容

使用块的网络(VGG)

VGG 模型是什么?

VGG (Visual Geometry Group) 是由牛津大学的 Visual Geometry Group 开发的,它是一个深度卷积神经网络架构。VGG 在 2014 年的 ImageNet 挑战赛中取得了很好的成绩。

VGG 的主要特点:

  1. 均匀的架构:VGG 的一个显著特点是其均匀的结构。它由多个相同大小的卷积层组成,后面跟着池化层,然后是全连接层。
  2. 小的卷积核:VGG 只使用了 3×33 \times 3 的卷积核,这是其与其他网络的主要区别。使用小的卷积核可以减少参数数量,并允许网络有更多的卷积层。
  3. 深度:VGG 提出了多种不同深度的版本,如 VGG16 和 VGG19,其中数字表示网络中的层数。

使用块的思想

VGG 的一个关键思想是使用重复的块来构建网络。每个块包含数个卷积层,后面跟着一个池化层。随着网络深度的增加,卷积层的数量也会增加。

VGG 的结构: 以 VGG16 为例,它包含 13 个卷积层、3 个全连接层和 5 个池化层,总共 16 层。这就是为什么它被称为 VGG16 的原因。

PyTorch 中的 VGG: 在 PyTorch 的 torchvision.models 模块中,已经预先定义了 VGG 的各种变体,这使得我们可以很容易地使用和训练 VGG。

示例: 以下是一个简化的 VGG 块的实现示例:

import torch.nn as nn

def vgg_block(num_convs, in_channels, out_channels):
layers = []
for _ in range(num_convs):
layers.append(nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1))
layers.append(nn.ReLU())
in_channels = out_channels
layers.append(nn.MaxPool2d(kernel_size=2, stride=2)) # MaxPool2d 二维最大池化层
return nn.Sequential(*layers)

# 示例:使用 vgg_block 创建一个包含 2 个卷积层的 VGG 块
block = vgg_block(2, 64, 128)
print(block)

上述代码定义了一个 VGG 块,该块包含指定数量的卷积层,后面跟着一个池化层。这种模块化的设计使得 VGG 的架构非常清晰和易于实现。

使用 VGG 块的训练代码

首先,我们定义一个 VGG 块:

def vgg_block(num_convs, in_channels, out_channels):
layers = []
for _ in range(num_convs):
layers.append(nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1))
layers.append(nn.ReLU())
in_channels = out_channels
layers.append(nn.MaxPool2d(kernel_size=2, stride=2))
return nn.Sequential(*layers)

定义 21 层的 VGG 网络

接下来,我们使用 VGG 块来定义一个简化的 VGG 网络:

class VGGNet(nn.Module):
def __init__(self):
super(VGGNet, self).__init__()
# 使用 VGG 块构建网络
self.features = nn.Sequential(
vgg_block(2, 1, 64),
vgg_block(2, 64, 128),
vgg_block(3, 128, 256),
vgg_block(3, 256, 512),
vgg_block(3, 512, 512)
)
self.classifier = nn.Sequential(
nn.Linear(512*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.features(x)
x = x.view(x.size(0), -1)
x = self.classifier(x)
return x

来分析一下上面定义的 VGGNet 结构:

  1. 第一个 VGG 块:2 个卷积层 + 1 个池化层 = 3 层
  2. 第二个 VGG 块:2 个卷积层 + 1 个池化层 = 3 层
  3. 第三个 VGG 块:3 个卷积层 + 1 个池化层 = 4 层
  4. 第四个 VGG 块:3 个卷积层 + 1 个池化层 = 4 层
  5. 第五个 VGG 块:3 个卷积层 + 1 个池化层 = 4 层

以上,特征提取部分(self.features)总共有:3 + 3 + 4 + 4 + 4 = 18 层

接下来是分类器部分(self.classifier):

  1. 第一个全连接层 = 1 层
  2. 第二个全连接层 = 1 层
  3. 第三个全连接层(输出层)= 1 层

分类器部分总共有 3 层。

将特征提取部分和分类器部分加在一起,整个 VGGNet 结构总共有:18 + 3 = 21 层。

所以,上面定义的 VGGNet 是一个 21 层的网络。

提示

注意:因为下面的例子,我们使用了 transforms.Resize((227, 227)),所以在 VGG 网络中,我们得到的特征图大小为 7x7(而不是 VGG16 中的 1x1)。

定义 11 层的 VGG 网络

再来定义一个 11 层的 VGG 网络以满足本地训练的需求,我们可以采用以下结构:

  1. 两个 VGG 块,每个块包含 2 个卷积层和 1 个池化层。
  2. 两个全连接层和一个输出层。

这样,我们总共有:(2+1) + (2+1) = 6 层卷积和池化,再加上 3 层全连接,总共 9 层。为了达到 11 层,我们可以在第三个 VGG 块中添加 2 个卷积层。

以下是 11 层的 VGG 网络定义:

class VGG11(nn.Module):
def __init__(self):
super(VGG11, self).__init__()
# 使用 VGG 块构建网络
self.features = nn.Sequential(
vgg_block(2, 1, 64), # 2 conv + 1 pool = 3 layers
vgg_block(2, 64, 128), # 2 conv + 1 pool = 3 layers
vgg_block(2, 128, 256) # 2 conv + 1 pool = 3 layers
)
self.classifier = nn.Sequential(
nn.Linear(256*28*28, 4096), # 28x28 is the spatial size of the feature map at this stage
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.features(x)
# x.view(x.size(0), -1) 将 x 重塑为一个二维张量,其中第一个维度是批次大小,第二个维度是该批次中每个样本的所有其他数据。
x = x.view(x.size(0), -1)
x = self.classifier(x)
return x

注意:由于我们没有减少特征图的尺寸太多(只进行了 3 次池化操作),所以在分类器部分,我们需要处理较大的特征图尺寸(28x28)。这可能会导致模型有很多参数,所以在实际应用中,你可能需要根据数据集的大小和复杂性进行调整。

features 和 classifier

featuresclassifier 是典型的模块化设计,这种设计经常出现在卷积神经网络(CNN)中。以下是每个模块的主要目的:

  1. features: 这部分通常包含多个卷积层和池化层,其目的是从输入图像中提取有意义的特征。这些特征可能包括形状、纹理、颜色分布等。随着网络的深入,从简单的边缘和颜色块逐渐变为复杂的模式和对象部分。经过这一部分后,原始的图像数据已被转换为一个能表示其主要内容特征的特征图。

  2. classifier: 该部分通常包含一到多个全连接层,其目的是基于前面提取的特征对图像进行分类或回归。全连接层的权重学习如何组合这些特征来识别图像中的内容,例如验证码中的字符。

如下

Input Image
|
V
------------------
| features |
|-----------------|
| Conv -> ReLU |
| MaxPool |
| Conv -> ReLU |
| MaxPool |
| Conv -> ReLU |
| MaxPool |
------------------
|
V
(Feature Maps)
|
V
------------------
| classifier |
|-----------------|
| Flatten |
| Dropout |
| Fully Connected |
| ReLU |
| Dropout |
| Fully Connected |
------------------
|
V
Predictions (e.g., character probabilities)

训练 Fashion-MNIST 数据集

简化通道

由于 VGG-11 比AlexNet计算量更大,因此我们构建了一个通道数较少的网络,足够用于训练Fashion-MNIST数据集。

以下是一个简化的 VGG-11 网络,专门为 Fashion-MNIST 数据集设计:

class VGG11_Simplified(nn.Module):
def __init__(self):
super(VGG11_Simplified, self).__init__()
# 使用简化的 VGG 块构建网络
self.features = nn.Sequential(
vgg_block(1, 1, 16), # 1 conv + 1 pool = 2 layers
vgg_block(1, 16, 32), # 1 conv + 1 pool = 2 layers
vgg_block(2, 32, 64), # 2 conv + 1 pool = 3 layers
vgg_block(2, 64, 128), # 2 conv + 1 pool = 3 layers
nn.MaxPool2d(kernel_size=2, stride=2) # Additional pooling layer to reduce size to 7x7
)
self.classifier = nn.Sequential(
nn.Linear(128*7*7, 512),
nn.ReLU(),
nn.Dropout(0.5),
nn.Linear(512, 512),
nn.ReLU(),
nn.Dropout(0.5),
nn.Linear(512, 10)
)

def forward(self, x):
x = self.features(x)
x = x.view(x.size(0), -1)
x = self.classifier(x)
return x

在这个简化的版本中,我们减少了通道数,并保持了 VGG-11 的基本结构。这样,模型的参数数量会大大减少,从而更适合于小型数据集,如 Fashion-MNIST。

具体的代码

import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
import matplotlib.pyplot as plt

if torch.cuda.is_available():
print("CUDA (GPU support) is available and PyTorch can use GPUs!")
else:
print("CUDA is not available. PyTorch will use CPU.")

# 检查 CUDA 是否可用并定义设备
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

# 数据加载和预处理
transform = transforms.Compose([
transforms.Resize((227, 227)), # 放大图片到 227x227
transforms.ToTensor(),
transforms.Normalize((0.5,), (0.5,))
])

trainset = torchvision.datasets.FashionMNIST(
root='./data', train=True, download=True, transform=transform)
trainloader = torch.utils.data.DataLoader(
trainset, batch_size=64, shuffle=True)
testset = torchvision.datasets.FashionMNIST(
root='./data', train=False, download=True, transform=transform)
testloader = torch.utils.data.DataLoader(testset, batch_size=64, shuffle=False)

# 只使用训练集的 5% 进行训练
num_samples = len(trainset)
num_train = int(0.05 * num_samples)
subset_trainset, _ = torch.utils.data.random_split(trainset, [num_train, num_samples - num_train])

trainloader = torch.utils.data.DataLoader(subset_trainset, batch_size=64, shuffle=True)

# 定义 VGGNet 模型
def vgg_block(num_convs, in_channels, out_channels):
layers = []
for _ in range(num_convs):
layers.append(nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1))
layers.append(nn.ReLU())
in_channels = out_channels
layers.append(nn.MaxPool2d(kernel_size=2, stride=2))
return nn.Sequential(*layers)

class VGG11_Simplified(nn.Module):
def __init__(self):
super(VGG11_Simplified, self).__init__()
# 使用简化的 VGG 块构建网络
self.features = nn.Sequential(
vgg_block(1, 1, 16), # 1 conv + 1 pool = 2 layers
vgg_block(1, 16, 32), # 1 conv + 1 pool = 2 layers
vgg_block(2, 32, 64), # 2 conv + 1 pool = 3 layers
vgg_block(2, 64, 128), # 2 conv + 1 pool = 3 layers
nn.MaxPool2d(kernel_size=2, stride=2) # Additional pooling layer to reduce size to 7x7
)
self.classifier = nn.Sequential(
nn.Linear(128*7*7, 512),
nn.ReLU(),
nn.Dropout(0.5),
nn.Linear(512, 512),
nn.ReLU(),
nn.Dropout(0.5),
nn.Linear(512, 10)
)

def forward(self, x):
x = self.features(x)
x = x.view(x.size(0), -1)
x = self.classifier(x)
return x


model = VGG11_Simplified().to(device)

# 定义损失函数和优化器
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

train_losses = []
test_accuracies = []

# 训练模型
for epoch in range(10):
running_loss = 0.0
for i, data in enumerate(trainloader, 0):
inputs, labels = data[0].to(device), data[1].to(device)
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
running_loss += loss.item()
train_losses.append(running_loss/len(trainloader))
# 在每个 epoch 结束后计算测试集的准确率
correct = 0
total = 0
with torch.no_grad():
for data in testloader:
images, labels = data[0].to(device), data[1].to(device)
outputs = model(images)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
test_accuracies.append(100 * correct / total)

print(f"Epoch {epoch+1}, Loss: {running_loss/len(trainloader)}, Test Accuracy: {100 * correct / total}%")

# 绘制 trainLoss 和 testAccu
fig, ax1 = plt.subplots()

color = 'tab:red'
ax1.set_xlabel('Epochs')
ax1.set_ylabel('Train Loss', color=color)
ax1.plot(train_losses, color=color)
ax1.tick_params(axis='y', labelcolor=color)

ax2 = ax1.twinx()
color = 'tab:blue'
ax2.set_ylabel('Test Accuracy', color=color)
ax2.plot(test_accuracies, color=color)
ax2.tick_params(axis='y', labelcolor=color)

fig.tight_layout()
plt.show()

print("Finished Training")

# 评估模型
correct = 0
total = 0
with torch.no_grad():
for data in testloader:
images, labels = data[0].to(device), data[1].to(device)
outputs = model(images)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()

print(f"Accuracy on test set: {100 * correct / total}%")

可以发现,上面对数据集各种裁切都需要训练 25 分钟,可见 VGG 网络的复杂性。

输出:

CUDA (GPU support) is available and PyTorch can use GPUs!
Epoch 1, Loss: 1.8246486516709024, Test Accuracy: 53.72%
Epoch 2, Loss: 0.9519902429682143, Test Accuracy: 67.85%
Epoch 3, Loss: 0.7361984487543715, Test Accuracy: 72.97%
Epoch 4, Loss: 0.6280527615800817, Test Accuracy: 75.52%
Epoch 5, Loss: 0.5413550182859949, Test Accuracy: 78.26%
Epoch 6, Loss: 0.49899705293330743, Test Accuracy: 79.79%
Epoch 7, Loss: 0.463084487838948, Test Accuracy: 80.68%
Epoch 8, Loss: 0.44128942457919423, Test Accuracy: 81.61%
Epoch 9, Loss: 0.40404345380499007, Test Accuracy: 82.42%
Epoch 10, Loss: 0.38445990485079745, Test Accuracy: 83.43%

Finished Training
Accuracy on test set: 83.09%

为什么 VGG 模型这么慢

VGG-11 和 AlexNet 在设计上有几个关键的区别,这些区别可能导致 VGG-11 的计算量比 AlexNet 大得多:

  1. 深度:尽管它们都被称为“深度”网络,但 VGG-11 实际上比 AlexNet 更深。更深的网络通常意味着更多的计算。

  2. 卷积核大小:AlexNet 在其第一层使用了较大的 11x11 的卷积核,而 VGG 使用了更小的 3x3 的卷积核。尽管小的卷积核可以更精确地捕获图像的细节,但它们也意味着更多的卷积操作,因为每个卷积核覆盖的图像区域更小。

  3. 通道数:VGG 网络在其层中使用了更多的通道。更多的通道意味着更多的参数和更大的计算量。

  4. 全连接层:VGG 的全连接层有更多的神经元和参数。例如,VGG 的前两个全连接层都有 4096 个神经元,而 AlexNet 的相应层只有 2048 和 4096 个神经元。

  5. 参数数量:由于上述原因,VGG-11 的总参数数量比 AlexNet 多得多。更多的参数意味着更大的计算量和更长的训练时间。

  6. 池化操作:虽然池化操作可以减少特征图的尺寸,从而减少计算量,但 VGG 在其结构中使用了更多的卷积层和更少的池化层。这意味着 VGG 在其网络中保留了更大的特征图,这进一步增加了计算量。

  7. 优化和实现细节:除了上述结构差异外,不同的网络实现和优化也可能影响性能。例如,某些网络可能受益于特定的硬件优化或库。

总的来说,尽管 VGG-11 和 AlexNet 都是深度卷积神经网络,但由于它们的设计差异,VGG-11 的计算量和参数数量都比 AlexNet 大得多,这导致了更长的训练和推理时间。

定义训练模型的思路

这里使用精简版的 VGG 模型

# 定义模型
class SimpleCaptchaModel(nn.Module):
def __init__(self, num_classes=len(nums), num_chars=4):
super(SimpleCaptchaModel, self).__init__()

self.num_chars = num_chars
self.num_classes = num_classes

# 特征提取部分
self.features = nn.Sequential(
# 第一个卷积层:从3个通道 (RGB) 到32个通道
nn.Conv2d(3, 32, kernel_size=3, padding=1),
nn.ReLU(inplace=True), # 使用ReLU作为激活函数
nn.MaxPool2d(kernel_size=2, stride=2), # 最大池化减少空间尺寸

# 第二个卷积层:从32个通道到64个通道
nn.Conv2d(32, 64, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=2, stride=2),

# 第三个卷积层:从64个通道到128个通道
nn.Conv2d(64, 128, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=2, stride=2),
)

# 分类部分
self.classifier = nn.Sequential(
# 防止过拟合
nn.Dropout(0.5),
# 通过全连接层将特征映射到一个512维的空间
nn.Linear(128 * 16 * 16, 512),
nn.ReLU(inplace=True),
nn.Dropout(0.5),
# 将512维的特征映射到验证码的输出长度
nn.Linear(512, num_classes * num_chars),
)

def forward(self, x):
# 通过特征提取部分
x = self.features(x)
# x.size(0):获取张量 x 的第一个维度的大小,通常这对应于一个批次中的样本数量。
# -1:在 view() 函数中,-1 表示该维度的大小是通过其他维度的大小和张量的总元素数量来自动计算的。
# 展平特征以送入分类器(x.view(x.size(0), -1) 将 x 重塑为一个二维张量,其中第一个维度是批次大小,第二个维度是该批次中每个样本的所有其他数据)
x = x.view(x.size(0), -1)
# 通过分类部分
x = self.classifier(x)
# 调整输出的形状以适应字符分类
return x.view(x.size(0), self.num_chars, self.num_classes)


model = SimpleCaptchaModel().to(device)

全连接层的大小的计算?

下面详细地计算一下特征图的尺寸如何从输入的 128x128 尺寸变化到 16x16。

输入图像尺寸:128x128

  1. 第一个卷积层:使用 3x3 的卷积核和 1 的填充(padding),所以空间尺寸不会改变:

    • 输出尺寸:128x128 (但通道数变为32)

    接下来是一个 最大池化 层:

    • 池化核尺寸:2x2,步长:2
    • 输出尺寸:(128 ÷ 2) x (128 ÷ 2) = 64x64
  2. 第二个卷积层:使用 3x3 的卷积核和 1 的填充,空间尺寸不会改变:

    • 输出尺寸:64x64 (但通道数变为64)

    再接一个 最大池化 层:

    • 输出尺寸:(64 ÷ 2) x (64 ÷ 2) = 32x32
  3. 第三个卷积层:使用 3x3 的卷积核和 1 的填充,空间尺寸仍然不会改变:

    • 输出尺寸:32x32 (但通道数变为128)

    最后一个 最大池化 层:

    • 输出尺寸:(32 ÷ 2) x (32 ÷ 2) = 16x16

所以,经过这三个卷积层和三个池化层之后,特征图的尺寸从 128x128 减小到了 16x16,而通道数增加到了128。

全连接层的降维

在这个模型中,"512维" 是指一个向量或张量的一个维度有512个元素。

当我们说某个全连接层(或密集层)输出一个512维的向量时,意思是该层的输出特征数量为512。具体地说,这个层的每个神经元都会输出一个值,所以512个神经元会输出一个长度为512的向量。

在上面的模型中:

nn.Linear(128 * 16 * 16, 512)

这个全连接层将一个大小为128 * 16 * 16(也就是 32768)的向量映射到一个大小为 512 的向量。这里的 32768 来自于前面的特征提取部分输出的特征图尺寸:16x16 的空间尺寸乘以 128 的通道数。这个全连接层通过其权重矩阵实现了这种映射。

这种映射或降维的目的在于减少参数数量(从 32768 维降到 512 维会大大减少参数),同时在较低的维度中捕获输入特征的主要信息。512维通常是一个经验选择,表示我们想在此步骤中保留的特征数量。

下面具体介绍下如何通过全连接层将一个32768维的向量转换为一个512维的向量。

首先这个层的主要组件是权重矩阵和偏置向量。

  1. 权重矩阵:这个矩阵的尺寸是 512,32768512, 32768。为什么这样?因为我们想将一个 32768 维的向量转换为一个 512 维的向量。所以,每一行都包含了 32768 个权重,这些权重用于计算新向量的一个特定的维度。
  2. 偏置向量:这是一个 512 维的向量,每个维度都有一个偏置值。

以下是一个具体的例子

我们有一个 32768×51232768 \times 512 的权重矩阵和一个 512 维的偏置向量。我会重新解释并提供一个高级的例子来演示如何从 32768 维降至 512 维。

首先,假设我们有一个 32768 维的输入向量:a1,a2,...,a32768a_1, a_2, ... , a_{32768}

接下来,我们有一个权重矩阵,其尺寸是512×32768512 \times 32768。这意味着我们有512行,每行都有32768个权重。

为了方便起见,让我们考虑计算输出向量中的第一个维度值。我们将使用权重矩阵的第一行和输入向量进行点积操作:

o1=i=132768(ai×w1i)+b1o_1 = \sum_{i=1}^{32768} (a_i \times w_{1i}) + b_1

其中,w1iw_{1i} 是权重矩阵第一行的第i个权重,b1b_1 是偏置向量的第一个值。

为了得到 512 维输出向量中的其他维度值,我们将进行类似的计算,但是使用权重矩阵的其他行。

因此,最终的输出向量是:

o=[o1,o2,...,o512]o = [o_1, o_2, ... , o_{512}]

这样,通过权重矩阵和偏置向量的计算,我们成功地将一个32768维的输入向量转换为一个512维的输出向量。这种降维是由权重矩阵中的值决定的,这些值在模型训练期间通过反向传播算法进行优化。

卷积层是怎么工作的?

下面的代码为什么能从三个通道转成 32 个输出通道

# 第一个卷积层:从3个通道 (RGB) 到32个通道
nn.Conv2d(3, 32, kernel_size=3, padding=1),

在代码中:

  • 3: 输入通道数(例如,一个RGB图像有3个通道: 红、绿、蓝)。
  • 32: 输出通道数。
  • kernel_size=3: 卷积核的大小是3x3。
  • padding=1: 在原始输入周围添加一层0值填充。

让我们通过一个简化的例子来理解如何从3个输入通道获得32个输出通道:

  1. 输入: 假设我们有一个3x3的RGB图像,其实就是3个3x3的矩阵,每个矩阵代表一个颜色通道。

    R        G        B
    1 2 3 4 5 6 7 8 9
    4 5 6 7 8 9 1 2 3
    7 8 9 1 2 3 4 5 6
  2. 卷积核: 对于这个3x3的输入,我们有32组卷积核。每组卷积核包含3个3x3的卷积核(与输入的3个通道相匹配)。

    以第1组卷积核为例(我们有32组这样的卷积核):

    Kr       Kg       Kb
    -1 0 1 -1 0 1 -1 0 1
    0 1 2 -1 0 1 0 1 2
    -1 0 1 -1 0 1 -1 0 1
  3. 卷积操作: 当我们对RGB图像应用这组卷积核时,每个卷积核与其对应的颜色通道进行卷积,产生3个3x3的输出特征图。然后,这3个输出特征图会被逐元素加在一起,形成一个3x3的输出特征图。

    (1,1) 位置为例(左上角):

    R: 1 * (-1) + 2 * 0 + 3 * 1 + 4 * 0 + 5 * 1 + 6 * 2 + 7 * (-1) + 8 * 0 + 9 * 1 = 16
    G: 4 * (-1) + 5 * 0 + 6 * 1 + 7 * 0 + 8 * 1 + 9 * 2 + 1 * (-1) + 2 * 0 + 3 * 1 = 18
    B: 7 * (-1) + 8 * 0 + 9 * 1 + 1 * 0 + 2 * 1 + 3 * 2 + 4 * (-1) + 5 * 0 + 6 * 1 = 10

    结果加起来得到 16 + 18 + 10 = 44

    所以,输出特征图的 (1,1) 位置的值是 44

  4. 输出: 由于我们有32组这样的卷积核,因此最终会得到32个这样的3x3输出特征图,即32个输出通道。

总结: 每组卷积核(3个3x3卷积核)都负责生成一个输出通道。由于有32组这样的卷积核,我们得到32个输出通道。

nn.MaxPool2d 的应用

上面的代码中,nn.MaxPool2d 不是会降低特征数量吗?为什么给下一层的输入还能有 32?

  # 特征提取部分
self.features = nn.Sequential(
# 第一个卷积层:从3个通道 (RGB) 到32个通道
nn.Conv2d(3, 32, kernel_size=3, padding=1),
nn.ReLU(inplace=True), # 使用ReLU作为激活函数
nn.MaxPool2d(kernel_size=2, stride=2), # 最大池化减少空间尺寸
# 第二个卷积层:从32个通道到64个通道
nn.Conv2d(32, 64, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=2, stride=2),
# 第三个卷积层:从64个通道到128个通道
nn.Conv2d(64, 128, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=2, stride=2),
)

首先,理解 nn.MaxPool2d 的作用是非常重要的。nn.MaxPool2d 是一个池化层,它对输入特征图进行下采样,减少其空间尺寸,但不会改变通道数。

具体解释以下几点:

  1. 空间尺寸:当我们说空间尺寸,我们通常指的是特征图的宽度和高度。例如,一个具有 [32, 64, 16, 16] 形状的张量表示有32个样本,每个样本有64个通道,每个通道的空间尺寸是16x16。
  2. 通道数:通道数指的是特征图的深度。在上述张量中,有64个通道。
  3. MaxPool2d的作用nn.MaxPool2d(kernel_size=2, stride=2) 的作用是将输入特征图的空间尺寸减半,但它不会改变通道数。例如,如果输入是 [32, 64, 16, 16],经过池化后,输出会是 [32, 64, 8, 8]

所以,回到问题,当在第一个卷积层后应用 nn.MaxPool2d 时,特征图的通道数不会改变,仍然是32。空间尺寸减半是为了减少计算量和模型的参数数量,同时使模型具有更强的平移不变性。

介绍 nn.MaxPool2d 计算过程

nn.MaxPool2d 它从一个局部区域(由 kernel_size 定义)中取最大值作为该区域的代表。这个操作减少了特征图的尺寸,同时保留了关键的特征。

下面,我会使用一个简单的数学示例来展示如何在一小段特征图上应用 nn.MaxPool2d

假设我们有以下的4x4特征图,作为某一卷积层的输出:

5  7  2  3
6 1 8 4
7 9 3 6
5 2 4 7

我们使用 nn.MaxPool2d(kernel_size=2, stride=2)。这意味着我们将使用一个2x2的窗口,在特征图上滑动,每次滑动2个单位(这也是为什么它会减少特征图尺寸的原因),并从每个2x2的窗口中选取最大值。

应用上述的池化操作后得到:

| 5  7 |  2  3      7
| 6 1 | 8 4 -> 8
-------|-------
| 7 9 | 3 6 9
| 5 2 | 4 7 -> 7

所以,经过最大池化后,我们得到的输出是:

7  8
9 7

这样,我们的特征图从4x4变成了2x2,但我们依然保留了每个2x2区域的最大值,这通常被视为该区域的关键特征。

在你提供的代码中,每一层的 nn.MaxPool2d 都将特征图的高度和宽度减少到原来的一半。所以,如果你的输入图像尺寸是 32x32,在经过三层最大池化后,特征图的尺寸会变成 4x4。