VGG16在图像分类中的应用:网络结构、数学原理与代码实践

VGG网络:核心思想与实现

在深度学习的卷积神经网络(CNN)架构中,VGG网络是一个经典且影响深远的模型。VGG由牛津大学的研究人员于2014年提出,专注于通过增加网络深度来提高模型的表现。这篇博客将详细探讨VGG网络的核心思想、数学原理以及它的代码实现。

VGG 网络简介

VGG网络的全称是Visual Geometry Group,最著名的模型是VGG16VGG19,分别包含16层和19层可训练的权重。

VGG16 和 VGG19 中的 “16” 和 “19” 分别代表网络中包含的 可训练权重层(即卷积层和全连接层) 的总数。

  • VGG16 由 13 个卷积层和 3 个全连接层组成,因此总共有 16 个具有可训练权重的层。
  • VGG19 则有 16 个卷积层和 3 个全连接层,总共有 19 个具有可训练权重的层。

需要注意的是,池化层不包含在这个计数中,因为池化层没有可训练参数

相比于之前的网络(如LeNet或AlexNet),VGG最显著的贡献在于引入了一种简单但非常有效的 网络块设计 ,即所谓的VGG块

这种设计与软件开发中的 抽象概念 类似——在函数封装完成后,开发者只需关注输入和输出,而不必关心内部实现细节。VGG网络通过反复堆叠这些抽象的VGG块,简化了网络的构建过程,同时通过增加深度来提升模型的表现。

VGG的核心思想:VGG块

VGG的关键是使用一组相同大小的小卷积核(通常为 3 × 3 3 \times 3 3×3)和最大池化层( 2 × 2 2 \times 2 2×2)堆叠组合形成的模块化结构。每个VGG块包含两个到三个连续的卷积层,接着是一个最大池化层。这种模块化设计简化了网络的构造,使得我们可以通过增加更多的卷积块来扩展网络的深度。

对于输入特征图中的某一位置 ( i , j ) (i, j) (i,j),卷积操作的核心思想是:使用一个小的卷积核在输入特征图上 滑动,并在每个位置上计算局部区域内的 加权和 。这里,我们通过一个 3 × 3 3 \times 3 3×3 的卷积核来解释这个过程。

卷积操作详解

假设输入特征图 x x x 的大小为 H × W H \times W H×W(高度 H H H 和宽度 W W W),每一个像素点 ( i , j ) (i, j) (i,j) 都有一个对应的数值(特征)。卷积核 W W W 的大小为 3 × 3 3 \times 3 3×3,它有9个可训练的权重(每个位置对应一个权重值)。卷积操作的过程可以理解为:

  1. 局部区域的加权求和:卷积核在特征图上滑动,在每个滑动位置,选取 3 × 3 3 \times 3 3×3 范围内的像素值,并与卷积核的权重进行 逐元素相乘 ,然后将这些乘积相加,得到该位置的输出。

  2. 逐点滑动的过程:对于特征图中的每一个位置 ( i , j ) (i, j) (i,j),我们会选择以 ( i , j ) (i, j) (i,j) 为中心的一个 3 × 3 3 \times 3 3×3 区域,进行加权求和,这个操作可以用以下公式描述:

    y ( i , j ) = ∑ m = − 1 1 ∑ n = − 1 1 W ( m , n ) ⋅ x ( i + m , j + n ) y(i, j) = \sum_{m=-1}^{1} \sum_{n=-1}^{1} W(m,n) \cdot x(i+m, j+n) y(i,j)=m=11n=11W(m,n)x(i+m,j+n)

    其中:

    • y ( i , j ) y(i, j) y(i,j) 是输出特征图在位置 ( i , j ) (i, j) (i,j) 处的值。
    • W ( m , n ) W(m, n) W(m,n) 是卷积核在位置 ( m , n ) (m, n) (m,n) 处的权重,卷积核的大小为 3 × 3 3 \times 3 3×3,所以 m , n ∈ { − 1 , 0 , 1 } m, n \in \{-1, 0, 1\} m,n{ 1,0,1}
    • x ( i + m , j + n ) x(i+m, j+n) x(i+m,j+n) 是输入特征图在位置 ( i + m , j + n ) (i+m, j+n) (i+m,j+n) 处的值,表示输入图像的局部区域。
  3. 示例解释:假设卷积核的大小为 3 × 3 3 \times 3 3×3,它会覆盖输入特征图中的 3 × 3 3 \times 3 3×3 区域。例如,对于位置 ( i , j ) (i, j) (i,j),选取的区域包括输入图像的像素点 ( i − 1 , j − 1 ) (i-1, j-1) (i1,j1) ( i + 1 , j + 1 ) (i+1, j+1) (i+1,j+1) 的9个像素点。对于这个区域的每个像素点,都与卷积核的相应权重值相乘,最后将这些乘积求和,得出该位置的输出值 y ( i , j ) y(i, j) y(i,j)

这个过程可以看作是一种加权滑动窗口,即卷积核是一个大小固定的窗口,它在输入图像上滑动,并在每个位置执行相同的加权求和操作。

为什么 3 × 3 3 \times 3 3×3 卷积核更有效?

通过这个 3 × 3 3 \times 3 3×3 的卷积核,卷积操作在每个位置只需要执行9个乘法和加法操作,这大大减少了计算量。相比之下,如果使用较大的卷积核(如 5 × 5 5 \times 5 5×5),每个位置需要进行25次乘法和加法操作,计算复杂度显著增加。而 3 × 3 3 \times 3 3×3 的卷积核通过逐层堆叠,能够在保留局部细节的同时保持较低的计算成本。

此外,连续使用两个 3 × 3 3 \times 3 3×3 的卷积核(深度增加)相当于一个 5 × 5 5 \times 5 5×5 的感受野,但参数更少,计算更加高效,同时还能保留更多层次化的细节。

参数比较

  1. 单个 5 × 5 5 \times 5 5×5 卷积

    • 参数量: 5 × 5 = 25 5 \times 5 = 25 5×5=25 个权重。
    • 感受野: 5 × 5 5 \times 5 5×5,可以捕捉更大区域的信息,但计算复杂度高。
  2. 两个连续的 3 × 3 3 \times 3 3×3 卷积

    • 参数量: 3 × 3 = 9 3 \times 3 = 9 3×3=9,每个卷积核9个权重,两个卷积核共有 9 + 9 = 18 9 + 9 = 18 9+9=18 个权重。
    • 感受野:等效于 5 × 5 5 \times 5 5×5,即经过两次 3 × 3 3 \times 3 3×3 卷积后,感受野扩展到 5 × 5 5 \times 5 5×5,但参数量减少了,计算更高效。

总结起来, 3 × 3 3 \times 3 3×3 卷积核的优势在于能够通过更少的参数捕捉局部信息,并且通过堆叠多个卷积层,可以逐步扩展感受野,达到更大卷积核的效果。因此,它既保持了计算效率,也能够逐步捕捉到复杂的全局特征。

  1. 感受野控制:一个 3 × 3 3 \times 3 3×3 的卷积核依然可以捕捉足够的局部信息,同时不会带来过多的计算开销。

  2. 参数效率:多个小卷积核的堆叠可以达到与大卷积核类似的效果。例如,两个连续的 3 × 3 3 \times 3 3×3 卷积相当于一个 5 × 5 5 \times 5 5×5 的卷积核感受野,但具有更少的参数和更高的计算效率。这不仅减少了模型的复杂度,还保留了丰富的特征表达能力。

  3. 深但窄的效果更好:相较于更宽、更大的卷积核,VGG选择 增加网络的深度 ,但保持每个卷积核较小且固定为 3 × 3 3 \times 3 3×3,以此实现更加 精细 的特征提取。这种设计思想通过逐层堆叠多个卷积层,提供了更深的感受野,能够逐级提取复杂的图像特征。

数学原理

卷积操作

卷积是CNN的核心操作之一。假设输入为一个二维矩阵(图像),卷积操作通过一个小的权重矩阵(卷积核)逐步滑过输入的每个区域,对局部进行加权求和并输出特征图。这个过程可以表达为:

y i , j = ∑ k = 1 h ∑ l = 1 w W k , l ⋅ x i + k , j + l y_{i,j} = \sum_{k=1}^{h} \sum_{l=1}^{w} W_{k,l} \cdot x_{i+k, j+l} yi,j=k=1hl=1wWk,lxi+k,j+l

其中, y i , j y_{i,j} yi,j 是输出特征图的值, W k , l W_{k,l} Wk,l 是卷积核权重, x i + k , j + l x_{i+k,j+l} xi+k,j+l 是输入矩阵在位置 ( i + k , j + l ) (i+k, j+l) (i+k,j+l) 的值。卷积核的大小通常是 3 × 3 3 \times 3 3×3,即 h = 3 , w = 3 h = 3, w = 3 h=3,w=3

最大池化层

最大池化层的作用是对特征图进行下采样,减少数据的空间维度,同时保留最重要的特征信息。它对局部区域内的值取最大值:

y i , j = max ⁡ ( x i : i + 2 , j : j + 2 ) y_{i,j} = \max(x_{i:i+2,j:j+2}) yi,j=max(xi:i+2,j:j+2)

最大池化通常使用 2 × 2 2 \times 2 2×2 的窗口,并以步长为2进行滑动。这样可以将输入特征图的尺寸减半,减少后续计算。

VGG 网络结构详解

VGG16为例,整个网络通过深度卷积层和池化层的重复组合来构建。VGG16的设计巧妙之处在于通过堆叠小卷积核最大池化层,实现了对图像特征的逐层提取,同时保持了计算效率和模型结构的简洁。

VGG16 的网络分块设计

VGG16可以划分为5个卷积块,每个块中包含若干个 3 × 3 3 \times 3 3×3 的卷积层,紧随其后的还有一个 2 × 2 2 \times 2 2×2 的最大池化层。每个卷积块的作用是逐层提取图像的局部特征,并通过池化层逐渐减小特征图的空间尺寸,从而控制计算复杂度。

卷积块完成特征提取后,最后的输出会通过3个全连接层,最后通过一个Softmax层输出图像的分类结果。下文将具体介绍每个卷积块的结构和网络的整体流程。

具体结构解释
  1. 输入图像:大小为 224 × 224 × 3 224 \times 224 \times 3 224×224×3(假设输入的是RGB图像,包含3个颜色通道)。

  2. 卷积块1

    • 2个 3 × 3 3 \times 3 3×3 的卷积层,每层卷积核数量为64个。
    • 1个 2 × 2 2 \times 2 2×2 最大池化层(步长为2),用于对特征图的下采样。
    • 输出特征图的大小变为 112 × 112 × 64 112 \times 112 \times 64 112×112×64(由于池化层将尺寸减半,通道数为64)。
  3. 卷积块2

    • 2个 3 × 3 3 \times 3 3×3 的卷积层,每层卷积核数量为128个。
    • 1个 2 × 2 2 \times 2 2×2 最大池化层
    • 输出特征图大小变为 56 × 56 × 128 56 \times 56 \times 128 56×56×128(继续减半,通道数增至128)。
  4. 卷积块3

    • 3个 3 × 3 3 \times 3 3×3 的卷积层,每层卷积核数量为256个。
    • 1个 2 × 2 2 \times 2 2×2 最大池化层
    • 输出特征图大小变为 28 × 28 × 256 28 \times 28 \times 256 28×28×256(尺寸减半,通道数增至256)。
  5. 卷积块4

    • 3个 3 × 3 3 \times 3 3×3 的卷积层,每层卷积核数量为512个。
    • 1个 2 × 2 2 \times 2 2×2 最大池化层
    • 输出特征图大小变为 14 × 14 × 512 14 \times 14 \times 512 14×14×512
  6. 卷积块5

    • 3个 3 × 3 3 \times 3 3×3 的卷积层,每层卷积核数量为512个。
    • 1个 2 × 2 2 \times 2 2×2 最大池化层
    • 输出特征图大小变为 7 × 7 × 512 7 \times 7 \times 512 7×7×512(经过多次池化操作,图像尺寸逐渐减小,但特征通道数不断增加)。
  7. 全连接层

    • 第一个全连接层:有4096个神经元。
    • 第二个全连接层:同样有4096个神经元。
    • 第三个全连接层:最终输出1000个神经元,对应ImageNet中的1000个分类类别。
  8. Softmax分类层:将输出的神经元值转化为概率,给出分类结果。

关键点解释

1. 为什么叫VGG16?
  • VGG16中的“16”表示网络中具有16层可训练的参数层,这些层包括13个卷积层和3个全连接层。池化层不算在这16层之内,因为它们没有可训练的参数,只是用来进行下采样。
2. 重复使用 3 × 3 3 \times 3 3×3 小卷积核
  • VGG16选择使用小尺寸的 3 × 3 3 \times 3 3×3 卷积核。相比于更大的卷积核(如 5 × 5 5 \times 5 5×5 7 × 7 7 \times 7 7×7),小卷积核的优势在于可以捕捉细致的局部特征,同时减少计算开销。
  • 多个小卷积核的堆叠还可以扩大感受野。例如,两个连续的 3 × 3 3 \times 3 3×3 卷积核相当于一个 5 × 5 5 \times 5 5×5 的感受野,但具有更少的参数量。
3. 最大池化层的作用
  • 最大池化层的作用是逐渐减少特征图的空间尺寸,从而减小计算复杂度,同时保留重要的特征。这有助于控制模型规模并防止过拟合。
4. 深度的设计
  • VGG16的设计理念强调 深度优先,即通过增加网络的层数来提高性能。深度增加使得网络可以逐层提取更加抽象、复杂的特征,从而提高分类精度。
5. 全连接层的作用
  • 卷积块完成图像特征的提取后,最后通过3个全连接层对提取的特征进行全局处理,将高维特征映射到分类空间。这些全连接层将最终的卷积特征压缩成输出分类概率。

VGG 网络的代码实现

我们可以按照VGG网络的结构在CIFAR-10数据集上实现VGG16网络。CIFAR-10数据集包含10个分类任务,每张图像大小为 32 × 32 × 3 32 \times 32 \times 3 32×32×3,因此相比于ImageNet,输入图像的分辨率要小得多。为了适应这个较小的输入图像尺寸,我们会对VGG16网络稍作调整。

接下来,我将展示如何实现VGG16在CIFAR-10数据集上的训练,并绘制和保存训练过程中损失和准确率的变化曲线。

1. 准备数据和依赖库

首先导入相关库,并准备CIFAR-10数据集。

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

# 数据预处理:归一化CIFAR-10数据
transform = transforms.Compose([
    transforms.RandomHorizontalFlip(),  # 随机水平翻转
    transforms.RandomCrop(32, padding=4),  # 随机裁剪
    transforms.ToTensor(),
    transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010))  # 标准化
])

# 加载CIFAR-10训练和测试数据集
trainset = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=128, shuffle=True, num_workers=2)

testset = torchvision.datasets.CIFAR10(root='./data', train=False, download=True, transform=transform)
testloader = torch.utils.data.DataLoader(testset, batch_size=100, shuffle=False, num_workers=2)

# CIFAR-10的分类类别
classes = ('plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck')

2. 定义VGG16模型

接下来,我们定义适用于CIFAR-10的VGG16网络。由于CIFAR-10图像的输入大小为 32 × 32 32 \times 32 32×32,因此我们不需要与ImageNet相同的网络深度,主要的修改是在全连接层部分。

# 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.BatchNorm2d(out_channels))  # 加入BatchNorm加速收敛
        layers.append(nn.ReLU(inplace=True))
        in_channels = out_channels
    layers.append(nn.MaxPool2d(kernel_size=2, stride=2))
    return nn.Sequential(*layers)

# VGG16模型定义
class VGG16(nn.Module):
    def __init__(self):
        super(VGG16, self).__init__()
        self.conv_layers = nn.Sequential(
            vgg_block(2, 3, 64),
            vgg_block(2, 64, 128),
            vgg_block(3, 128, 256),
            vgg_block(3, 256, 512),
            vgg_block(3, 512, 512)
        )
        self.fc_layers = nn.Sequential(
            nn.Linear(512 * 1 * 1, 4096),
            nn.ReLU(inplace=True),
            nn.Dropout(0.5),
            nn.Linear(4096, 4096),
            nn.ReLU(inplace=True),
            nn.Dropout(0.5),
            nn.Linear(4096, 10)  # CIFAR-10是10类分类
        )

    def forward(self, x):
        x = self.conv_layers(x)
        x = torch.flatten(x, 1)  # 展平特征图为一维
        x = self.fc_layers(x)
        return x

# 实例化VGG16模型
net = VGG16().cuda()

3. 训练和评估函数

定义训练和评估模型的函数,记录每个epoch的训练损失、测试损失、训练准确率和测试准确率。

def train(net, trainloader, criterion, optimizer, device):
    net.train()
    running_loss = 0.0
    correct = 0
    total = 0
    for inputs, labels in trainloader:
        inputs, labels = inputs.to(device), labels.to(device)

        optimizer.zero_grad()

        outputs = net(inputs)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()

        running_loss += loss.item()
        _, predicted = outputs.max(1)
        total += labels.size(0)
        correct += predicted.eq(labels).sum().item()

    train_loss = running_loss / len(trainloader)
    train_accuracy = 100. * correct / total
    return train_loss, train_accuracy

def test(net, testloader, criterion, device):
    net.eval()
    running_loss = 0.0
    correct = 0
    total = 0
    with torch.no_grad():
        for inputs, labels in testloader:
            inputs, labels = inputs.to(device), labels.to(device)

            outputs = net(inputs)
            loss = criterion(outputs, labels)

            running_loss += loss.item()
            _, predicted = outputs.max(1)
            total += labels.size(0)
            correct += predicted.eq(labels).sum().item()

    test_loss = running_loss / len(testloader)
    test_accuracy = 100. * correct / total
    return test_loss, test_accuracy

4. 训练过程

接下来,进行模型训练,记录每个epoch的训练和测试损失以及准确率。

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(net.parameters(), lr=0.001)

num_epochs = 20
train_losses, test_losses = [], []
train_accuracies, test_accuracies = [], []

for epoch in range(num_epochs):
    train_loss, train_accuracy = train(net, trainloader, criterion, optimizer, device)
    test_loss, test_accuracy = test(net, testloader, criterion, device)

    train_losses.append(train_loss)
    test_losses.append(test_loss)
    train_accuracies.append(train_accuracy)
    test_accuracies.append(test_accuracy)

    print(f"Epoch [{
      
      epoch+1}/{
      
      num_epochs}] | "
          f"Train Loss: {
      
      train_loss:.4f} | Test Loss: {
      
      test_loss:.4f} | "
          f"Train Acc: {
      
      train_accuracy:.2f}% | Test Acc: {
      
      test_accuracy:.2f}%")

5. 绘制损失和准确率曲线

最后,绘制并保存训练过程中的损失和准确率曲线。

# 绘制损失和准确率曲线
def plot_metrics(train_losses, test_losses, train_accuracies, test_accuracies, save_path='training_metrics.png'):
    epochs = range(1, len(train_losses) + 1)

    # 绘制损失曲线
    plt.figure(figsize=(12, 5))

    plt.subplot(1, 2, 1)
    plt.plot(epochs, train_losses, label='Train Loss')
    plt.plot(epochs, test_losses, label='Test Loss')
    plt.title('Loss over Epochs')
    plt.xlabel('Epochs')
    plt.ylabel('Loss')
    plt.legend()

    # 绘制准确率曲线
    plt.subplot(1, 2, 2)
    plt.plot(epochs, train_accuracies, label='Train Accuracy')
    plt.plot(epochs, test_accuracies, label='Test Accuracy')
    plt.title('Accuracy over Epochs')
    plt.xlabel('Epochs')
    plt.ylabel('Accuracy (%)')
    plt.legend()

    # 保存图像到指定路径
    plt.savefig(save_path)
    print(f"Training metrics saved as {
      
      save_path}")

# 调用函数并保存图片
plot_metrics(train_losses, test_losses, train_accuracies, test_accuracies, save_path='training_metrics.png')

6. 运行结果

训练完成后,程序将保存训练过程中的损失和准确率曲线,文件名为 training_metrics.png。通过这些曲线,可以直观地观察模型在训练和测试阶段的表现,如损失的变化和分类精度的提升。

在这里插入图片描述

  • 左图展示了训练损失和测试损失随 epoch 变化的趋势。整体上,损失值逐渐降低,表明模型在逐渐收敛。然而,测试损失的波动和趋缓可能暗示着模型在某些阶段出现了过拟合现象,即模型对训练数据表现得很好,但在测试集上的泛化能力稍有不足。

  • 右图显示了训练精度和测试精度的变化情况。可以看到,训练精度在持续提升,接近100%,而测试精度虽然整体有所提高,但在一些阶段出现了波动。这表明模型在训练集上表现得很好,但在验证或测试数据上偶尔会出现不稳定,可能与过拟合或训练过程中的不稳定因素有关。

以下是模型在最后五个 epoch 中的表现:

Epoch [95/100] | Train Loss: 0.0641 | Test Loss: 0.4824 | Train Acc: 98.18% | Test Acc: 89.10%
Epoch [96/100] | Train Loss: 0.2772 | Test Loss: 0.4335 | Train Acc: 92.51% | Test Acc: 87.72%
Epoch [97/100] | Train Loss: 0.1028 | Test Loss: 0.4511 | Train Acc: 96.80% | Test Acc: 89.29%
Epoch [98/100] | Train Loss: 0.0619 | Test Loss: 0.4485 | Train Acc: 98.08% | Test Acc: 89.66%
Epoch [99/100] | Train Loss: 0.0427 | Test Loss: 0.4716 | Train Acc: 98.69% | Test Acc: 90.07%
Epoch [100/100] | Train Loss: 0.0417 | Test Loss: 0.4384 | Train Acc: 98.73% | Test Acc: 90.17%

从最后五个epoch的结果可以看到,训练集上的损失和精度均趋于稳定,说明模型在训练集上的学习已经接近极限。而在测试集上,测试精度达到了90.17%,这表明模型的泛化性能较好,但仍然存在少量波动,可能是由于模型过拟合部分训练数据或是测试数据分布与训练集有所差异。

总的来说,VGG16在CIFAR-10上的表现令人满意,测试集上接近90%的准确率展示了深度卷积网络在图像分类任务中的有效性。然而,进一步的优化,例如正则化、数据增强或早停策略,可能有助于缓解过拟合并提高模型在测试集上的稳定性和表现。

结论

VGG网络通过简洁的卷积块设计,成功地加深了神经网络的层数,显著提升了图像分类任务的性能。其模块化结构、固定大小的卷积核以及最大池化操作,不仅提高了网络的表现,还保持了架构的简单性。

这个设计理念表明,简化设计往往可以带来更好的效果。通过专注于简单且可重复的模块,VGG网络避免了过度复杂的设计,使其更易于理解、记忆和使用。这也提醒我们,在面对问题时,追求简洁和实用的解决方案,往往能为后续的应用带来更多的便利和效率。

猜你喜欢

转载自blog.csdn.net/qq_22841387/article/details/143113582