【卷积神经网络】VGG原理与实践

概述

VGG是一种经典的卷积神经网络架构,由牛津大学的Visual Geometry Group(VGG)团队在2014年提出。该模型主要特点是使用一系列大小为3×3的卷积核、较深的网络层数以及周期性的池化层来获得更深层次的特征表示。其核心理念是在尽量保持较小感受野(即3×3卷积核)的前提下,通过增加网络的深度(层数)来提升模型的表示能力和性能

特点

  • 小卷积核,深层结构:VGG网络通常使用连续的多个3×3卷积层,以较小的卷积核来逐步增加特征通道数,从而实现更深且表现更优的网络结构
  • 统一的结构设计:网络结构简单、统一,基本由“卷积层块 + 最大池化层”叠加而成。各个卷积块的模式类似,使模型具有较好的通用性和可移植性
  • 性能优越,易迁移:VGG在ImageNet等大规模图像数据集上取得了优异的性能,且由于其结构清晰简单,常用于特征提取、迁移学习等下游任务
  • 代价是计算量大:VGG尽管效果优秀,但参数数量和计算量很大,对显存和计算资源有较高要求

网络结构

理解VGG网络结构

VGG网络结构的主要特点是使用多个3×3小卷积核的层堆叠构成卷积block,在block之间以max-pool下采样,在网络末端通过全连接层进行分类。通过多种配置(A~E),可以实现不同深度的模型,而VGG-16和VGG-19是其中最为广泛应用的版本

优点 

小卷积核(3 * 3 )与深层结构

VGG网络中使用多个3 * 3 的卷积核进行特征提取,代替使用较大的卷积核。使用多个3×3卷积层叠加能获得与更大卷积核相似的感受野,但参数更少、计算更高效,同时通过多层的叠加能学习到更细致的特征变化

简单来说就是把图像切割的更细了,可以对齐进行更好的分析

Block块状结构--多层卷积叠加

VGG将卷积层堆叠成block(模块),每个block包含2-3个(或更多)3×3卷积层,其输出通道数相同。这种统一的模块化设计让网络易于理解和扩展。每个block都专注于提取一定层次的特征,然后通过max-pool层进入下一个block,进一步提升特征抽象度

类似于将大任务拆分成小任务,然后再将部分小任务进行封装成块,这样就可以通过做不同块,实现最终大任务的目标

max-pool下采样层(在block间连接)

max-pool使得网络在不断加深的过程中,不会因为特征图过大而造成计算负担过重,还能让网络更快专注于全局特征而非局部细节

不断的将冗余内容剔除,将精力放在重要的部分

通道数逐层增加

VGG的卷积通道数从64开始,然后随着网络加深,增加到128、256、512。这意味着在网络的深层,模型可提取更加复杂和多维度的特征表示。在浅层使用较少通道来处理低级特征(如边缘、线条),在深层使用更多通道来学习更高级的特征(如物体形状、纹理和类别相关特征)

这个就像最初你观察一张图片时,可能只注意到简单的线条和颜色(通道数少)。随着你越看越细致,你开始关注到更多细节(不同纹理、局部形状),就像给你的大脑添加更多“观察工具”(增加通道数)来捕捉更丰富的信息

VGG可以配置多种版本

类似于根据不同的食谱(高级和初级),然后结合这些食谱的内容,最后做出完美的产品

VGG提出了多个配置版本(A、B、C、D、E),它们在卷积层数量、叠加方式上略有差异。VGG-16(D配置)和VGG-19(E配置)是最常用的版本,拥有更多的卷积层,表现出更强的特征提取能力

末端全连接层和分类输出

在前面步骤(block)里,已经多次过滤与提取信息,到了最后几步,就像将你的总结以更高层次的概念整合,比如把“颜色、纹理、形状、边缘”这些特点综合起来判断这是一只猫还是一条狗。全连接层就是大脑中负责综合判定的部分

当通过多重block提取特征后,VGG在末端使用全连接层和softmax进行分类。前两个全连接层通常有4096个神经元,将特征进行整合,最后一层输出分类结果

网络参数

以VGG-16进行学习

卷积层参数计算方法

卷积层的参数数量 = (卷积核宽度 × 卷积核高度 × 输入通道数) × 输出通道数 + 输出通道数对应的偏置参数

计算第一个VGG

输入图像为224×224×3(RGB三通道),第一个block包括两个3×3卷积层与一个2×2最大池化层

  • conv1_1:
    • 输入通道:3
    • 输出通道:64
    • 卷积核大小:3×3
      参数数 = (3×3×3)×64 + 64 = (27×64) + 64 = 1728 + 64 = 1792个参数
  • conv1_2:
    • 输入通道:64(因为它是在C1下面的卷积核)
    • 输出通道:64(第二个卷积层维持输出通道不变)
    • 卷积核大小:3×3
      参数数 = (3×3×64)×64 + 64 = (9×64×64) +64 = 36864 +64 = 36928个参数
  • 两个卷积层合计:1792 + 36928 = 38720 个参数
  • 经过池化层(2*2)无参数后,特征图变为112×112×64

全连接层计算

通过五个卷积模块后,特征图大小通常为7×7×512

  • 第一个全连接层(FC6):输入神经元数 = 7×7×512 = 25088
    • 若输出为4096,则参数量 = 25088×4096 + 4096 ≈ 约1亿级别参数
  • 第二个全连接层(FC7):输入4096, 输出4096
    参数量 = 4096×4096 +4096 = 约1677万参数
  • 第三个全连接层(FC8):输入4096, 输出1000(针对ImageNet 1000类)
    参数量 = 4096×1000 +1000 = 约4097k参数

实践

搭建模型

具体实现

与之前模型类似,只是将多个卷积封装成了块

class VGG16(nn.Module):
    def __init__(self):
        super(VGG16, self).__init__()
        # 第一块卷积模块:输入通道数为1(灰度图像),输出通道数为64,包含两层卷积和一层最大池化
        self.block1 = nn.Sequential(
            nn.Conv2d(in_channels=1, out_channels=64, kernel_size=3, padding=1),  # 卷积层1
            nn.ReLU(),  # 激活函数
            nn.Conv2d(in_channels=64, out_channels=64, kernel_size=3, padding=1),  # 卷积层2
            nn.ReLU(),  # 激活函数
            nn.MaxPool2d(kernel_size=2, stride=2)  # 最大池化,特征图尺寸减半
        )
        # 第二块卷积模块:输入64通道,输出128通道,包含两层卷积和一层最大池化
        self.block2 = nn.Sequential(
            nn.Conv2d(in_channels=64, out_channels=128, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.Conv2d(in_channels=128, out_channels=128, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2, stride=2)
        )
        # 第三块卷积模块:输入128通道,输出256通道,包含三层卷积和一层最大池化
        self.block3 = nn.Sequential(
            nn.Conv2d(in_channels=128, out_channels=256, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.Conv2d(in_channels=256, out_channels=256, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.Conv2d(in_channels=256, out_channels=256, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2, stride=2)
        )
        # 第四块卷积模块:输入256通道,输出512通道,包含三层卷积和一层最大池化
        self.block4 = nn.Sequential(
            nn.Conv2d(in_channels=256, out_channels=512, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2, stride=2)
        )
        # 第五块卷积模块:输入512通道,输出512通道,包含三层卷积和一层最大池化
        self.block5 = nn.Sequential(
            nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, padding=1),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2, stride=2)
        )

        # 全连接层:将卷积特征图展开并传入全连接层
        self.block6 = nn.Sequential(
            nn.Flatten(),  # 将特征展平为一维
            nn.Linear(7 * 7 * 512, 256),  # 全连接层1,输入7x7x512的特征向量,输出256维
            nn.ReLU(),
            nn.Linear(256, 128),  # 全连接层2,输入256维,输出128维
            nn.ReLU(),
            nn.Linear(128, 10),  # 全连接层3,输入128维,输出10分类
        )

        # 初始化权重
        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                nn.init.kaiming_normal_(m.weight, nonlinearity='relu')  # Kaiming初始化用于ReLU激活函数
                if m.bias is not None:
                    nn.init.constant_(m.bias, 0)  # 偏置初始化为0
            elif isinstance(m, nn.Linear):
                nn.init.normal_(m.weight, 0, 0.01)  # 全连接层权重初始化为标准正态分布
                if m.bias is not None:
                    nn.init.constant_(m.bias, 0)  # 偏置初始化为0

    def forward(self, x):
        # 前向传播:依次通过每个模块
        x = self.block1(x)
        x = self.block2(x)
        x = self.block3(x)
        x = self.block4(x)
        x = self.block5(x)
        x = self.block6(x)
        return x


if __name__ == "__main__":
    # 检查设备是否支持CUDA,如果有则使用GPU
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    # 实例化模型
    model = VGG16().to(device)
    # 使用torchsummary打印模型结构和参数信息
    print(summary(model, (1, 224, 224)))  # 假设输入为1通道(灰度图),大小为224x224

训练模型

训练结果(6次)

Epoch 0/5
----------
0 train loss:0.5047 train acc: 0.8143
0 val loss:0.3403 val acc: 0.8753
训练和验证耗费的时间23m51s
Epoch 1/5
----------
1 train loss:0.3190 train acc: 0.8793
1 val loss:0.3000 val acc: 0.8917
训练和验证耗费的时间47m37s
Epoch 2/5
----------
2 train loss:0.2806 train acc: 0.8948
2 val loss:0.2593 val acc: 0.9031
训练和验证耗费的时间71m23s
Epoch 3/5
----------
3 train loss:0.2481 train acc: 0.9076
3 val loss:0.2543 val acc: 0.9073
训练和验证耗费的时间95m8s
Epoch 4/5
----------
4 train loss:0.2195 train acc: 0.9176
4 val loss:0.2498 val acc: 0.9106
训练和验证耗费的时间118m50s
Epoch 5/5
----------
5 train loss:0.1977 train acc: 0.9277
5 val loss:0.2467 val acc: 0.9144
训练和验证耗费的时间142m33s

Process finished with exit code 0

实现代码

def train_val_data_process():
    train_data = FashionMNIST(root='./data',
                              train=True,
                              transform=transforms.Compose([transforms.Resize(size=224), transforms.ToTensor()]),
                              download=True)

    train_data, val_data = Data.random_split(train_data, [round(0.8*len(train_data)), round(0.2*len(train_data))])
    train_dataloader = Data.DataLoader(dataset=train_data,
                                       batch_size=14,
                                       shuffle=True,
                                       num_workers=2)

    val_dataloader = Data.DataLoader(dataset=val_data,
                                     batch_size=14,
                                     shuffle=True,
                                     num_workers=2)

    return train_dataloader, val_dataloader


def train_model_process(model, train_dataloader, val_dataloader, num_epochs):
    # 设置训练设备(GPU或CPU)
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    # 使用Adam优化器,学习率为0.001
    optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
    # 使用交叉熵损失函数
    criterion = nn.CrossEntropyLoss()
    # 将模型移动到训练设备
    model = model.to(device)
    # 复制当前模型的参数,用于保存最佳模型
    best_model_wts = copy.deepcopy(model.state_dict())

    # 初始化参数
    best_acc = 0.0
    train_loss_all = []
    val_loss_all = []
    train_acc_all = []
    val_acc_all = []
    since = time.time()

    for epoch in range(num_epochs):
        print("Epoch {}/{}".format(epoch, num_epochs-1))
        print("-"*10)

        # 初始化每个epoch的损失和准确率
        train_loss = 0.0
        train_corrects = 0
        val_loss = 0.0
        val_corrects = 0
        train_num = 0
        val_num = 0

        # 训练阶段
        for step, (b_x, b_y) in enumerate(train_dataloader):
            b_x = b_x.to(device)
            b_y = b_y.to(device)
            model.train()

            # 前向传播
            output = model(b_x)
            pre_lab = torch.argmax(output, dim=1)
            loss = criterion(output, b_y)

            # 反向传播和优化
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()

            # 累加损失和正确预测数
            train_loss += loss.item() * b_x.size(0)
            train_corrects += torch.sum(pre_lab == b_y.data)
            train_num += b_x.size(0)

        # 验证阶段
        for step, (b_x, b_y) in enumerate(val_dataloader):
            b_x = b_x.to(device)
            b_y = b_y.to(device)
            model.eval()

            with torch.no_grad():
                output = model(b_x)
                pre_lab = torch.argmax(output, dim=1)
                loss = criterion(output, b_y)

            # 累加损失和正确预测数
            val_loss += loss.item() * b_x.size(0)
            val_corrects += torch.sum(pre_lab == b_y.data)
            val_num += b_x.size(0)

        # 计算并记录每个epoch的平均损失和准确率
        train_loss_all.append(train_loss / train_num)
        train_acc_all.append(train_corrects.double().item() / train_num)
        val_loss_all.append(val_loss / val_num)
        val_acc_all.append(val_corrects.double().item() / val_num)

        print("{} train loss:{:.4f} train acc: {:.4f}".format(epoch, train_loss_all[-1], train_acc_all[-1]))
        print("{} val loss:{:.4f} val acc: {:.4f}".format(epoch, val_loss_all[-1], val_acc_all[-1]))

        # 如果当前验证准确率更高,则保存当前模型参数
        if val_acc_all[-1] > best_acc:
            best_acc = val_acc_all[-1]
            best_model_wts = copy.deepcopy(model.state_dict())

        # 计算并打印训练和验证所用时间
        time_use = time.time() - since
        print("训练和验证耗费的时间{:.0f}m{:.0f}s".format(time_use//60, time_use%60))

    # 加载最佳模型参数
    model.load_state_dict(best_model_wts)
    # 保存最佳模型参数到指定路径
    torch.save(best_model_wts, "E:\秋招就业\CNN卷积神经网络\测试用例\VGG16\\best_model.pth")

    # 将训练过程中的指标保存到DataFrame
    train_process = pd.DataFrame(data={"epoch":range(num_epochs),
                                       "train_loss_all":train_loss_all,
                                       "val_loss_all":val_loss_all,
                                       "train_acc_all":train_acc_all,
                                       "val_acc_all":val_acc_all,})

    return train_process

# 可视化
def matplot_acc_loss(train_process):
    # 显示每一次迭代后的训练集和验证集的损失函数和准确率
    plt.figure(figsize=(12, 4))
    plt.subplot(1, 2, 1)
    plt.plot(train_process['epoch'], train_process.train_loss_all, "ro-", label="Train loss")
    plt.plot(train_process['epoch'], train_process.val_loss_all, "bs-", label="Val loss")
    plt.legend()
    plt.xlabel("epoch")
    plt.ylabel("Loss")
    plt.subplot(1, 2, 2)
    plt.plot(train_process['epoch'], train_process.train_acc_all, "ro-", label="Train acc")
    plt.plot(train_process['epoch'], train_process.val_acc_all, "bs-", label="Val acc")
    plt.xlabel("epoch")
    plt.ylabel("acc")
    plt.legend()
    plt.show()

if __name__ == '__main__':
    # 加载需要的模型
    VGG16 = VGG16()
    # 加载数据集
    train_data, val_data = train_val_data_process()
    # 利用现有的模型进行模型的训练
    train_process = train_model_process(VGG16, train_data, val_data, num_epochs=6)
    matplot_acc_loss(train_process)

测试模型

测试的准确率为: 0.9091
预测值: Sneaker ------ 真实值: Sneaker
预测值: Ankle boot ------ 真实值: Ankle boot
预测值: T-shirt/top ------ 真实值: T-shirt/top
预测值: Sandal ------ 真实值: Sandal
预测值: Bag ------ 真实值: Bag
预测值: Coat ------ 真实值: Coat
预测值: Ankle boot ------ 真实值: Ankle boot
预测值: Dress ------ 真实值: Dress
预测值: Pullover ------ 真实值: Pullover
预测值: Trouser ------ 真实值: Trouser
预测值: Bag ------ 真实值: Bag
预测值: Bag ------ 真实值: Bag
预测值: Trouser ------ 真实值: Trouser
预测值: Pullover ------ 真实值: Pullover
预测值: Shirt ------ 真实值: Coat
预测值: Ankle boot ------ 真实值: Ankle boot
预测值: Shirt ------ 真实值: Shirt
预测值: Shirt ------ 真实值: Shirt
预测值: Bag ------ 真实值: Bag
预测值: Coat ------ 真实值: Coat
........................

测试代码

def test_data_process():
    test_data = FashionMNIST(root='./data',
                              train=False,
                              transform=transforms.Compose([transforms.Resize(size=224), transforms.ToTensor()]),
                              download=True)

    test_dataloader = Data.DataLoader(dataset=test_data,
                                       batch_size=1,
                                       shuffle=True,
                                       num_workers=0)
    return test_dataloader

def test_model_process(model, test_dataloader):
    # 设定测试所用到的设备,有GPU用GPU没有GPU用CPU
    device = "cuda" if torch.cuda.is_available() else 'cpu'

    # 讲模型放入到训练设备中
    model = model.to(device)

    # 初始化参数
    test_corrects = 0.0
    test_num = 0

    # 只进行前向传播计算,不计算梯度,从而节省内存,加快运行速度
    with torch.no_grad():
        for test_data_x, test_data_y in test_dataloader:
            # 将特征放入到测试设备中
            test_data_x = test_data_x.to(device)
            # 将标签放入到测试设备中
            test_data_y = test_data_y.to(device)
            # 设置模型为评估模式
            model.eval()
            # 前向传播过程,输入为测试数据集,输出为对每个样本的预测值
            output= model(test_data_x)
            # 查找每一行中最大值对应的行标
            pre_lab = torch.argmax(output, dim=1)
            # 如果预测正确,则准确度test_corrects加1
            test_corrects += torch.sum(pre_lab == test_data_y.data)
            # 将所有的测试样本进行累加
            test_num += test_data_x.size(0)

    # 计算测试准确率
    test_acc = test_corrects.double().item() / test_num
    print("测试的准确率为:", test_acc)

部分课件图片引用自“炮哥带你学”

### VGG 卷积神经网络架构 VGG 网络是由牛津大学视觉几何组(Visual Geometry Group)提出的经典卷积神经网络之一,因其出色的性能和简洁的设计而闻名。VGG 网络的主要特点是使用非常小的 (3×3) 卷积核,并通过堆叠多个这样的卷积层来增加网络深度。 #### 架构特点 - 使用 3x3 尺寸的小型卷积核,这种设计使得即使在网络较深的情况下也能有效提取特征[^2]。 - 每个卷积层后面通常跟着 ReLU 非线性激活函数。 - 多次应用最大池化操作以减少空间维度。 - 整体结构简单统一,在每一阶段重复相同类型的模块。 #### 结构细节 典型的 VGG16 和 VGG19 是两种最常见的版本: ##### VGG16 架构描述: ```plaintext Input: 224x224 RGB image Convolution layers: conv1_1 (64 filters), conv1_2 (64 filters) -> maxpooling conv2_1 (128 filters), conv2_2 (128 filters) -> maxpooling conv3_1 (256 filters), conv3_2 (256 filters), conv3_3 (256 filters) -> maxpooling conv4_1 (512 filters), conv4_2 (512 filters), conv4_3 (512 filters) -> maxpooling conv5_1 (512 filters), conv5_2 (512 filters), conv5_3 (512 filters) -> maxpooling Fully connected layers: fc6 (4096 units) fc7 (4096 units) Output layer: softmax classifier with number of classes as output size. ``` ##### VGG19 架构描述: VGG16 类似,但在第三、第四和第五个卷积块中各多了一个额外的卷积层。 #### 图形展示 虽然无法直接在此处绘制图形,但可以参考下述文字说明理解其布局。对于更直观的理解,建议查阅相关论文或教材中的插图,如 Fig. 4 提供了不同类型深层 CNN 架构分类的信息。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值