1️⃣ VGG介绍
Alexnet证明了神经网络变深是有效的,因此网络能不能更深更大?
VGG(visual geometry group)是由牛津大学提出的使用“块思想”的网络,通过使用循环和子程序可以很容易地在任何现代深度学习框架的代码中实现这些重复的架构。
VGG在2014
ImageNet图像分类与定位挑战赛 ILSVRC-2014中取得在分类任务第二,定位任务第一的优异成绩。
2️⃣ 创新
- LeNet(1995)
- 2卷积+池化层
- 2全连接层
- AlexNet(2012)
- 更大更深
- ReLu,Dropout,数据增强
- VGG(2014)
- 相比Alexnet ,VGG使用了更小的卷积核,更深的网络结构,是一个利用小卷积核不断加深网络的一次尝试。
3️⃣ 网络结构
注意:上面这张图最后fc4096应该是fc1000,画错了
总的来说,VGG16由13个卷积层和3个全连接层组成,一共16层(池化层不算层数,因为不包含需要学习的权重和偏置)。
其中的13个卷积层可以分成5个VGG块:
- 输入层:图片大小为
3×224×224
- 第1块:2个conv3-64组成(con3-64中3代表卷积核大小为3×3,64代表64个卷积核)
- 第一个卷积:64个3×3卷积核,padding=1,stride=1
输入:3×224×224
输出:64×224×224
再经过ReLu激活函数 - 第二个卷积:64个3×3卷积核,padding=1,stride=1
输入:64×224×224
输出:64×224×224
再经过ReLu激活函数 - 最大池化:2×2,stride=2,padding=0
输入:64×224×224
输出:64×112×112
- 第一个卷积:64个3×3卷积核,padding=1,stride=1
- 第2块:2个conv3-128组成
- 第一个卷积:128个3×3卷积核,padding=1,stride=1
输入:64×112×112
输出:128×112×112
再经过ReLu激活函数 - 第二个卷积:128个3×3卷积核,padding=1,stride=1
输入:128×112×112
输出:128×112×112
再经过ReLu激活函数 - 最大池化:2×2,stride=2,padding=0
输入:128×112×112
输出:128×56×56
- 第一个卷积:128个3×3卷积核,padding=1,stride=1
- 第3块:3个conv3-256组成
- 第一个卷积:256个3×3卷积核,padding=1,stride=1
输入:128×56×56
输出:256×56×56
再经过ReLu激活函数 - 第二个卷积:256个3×3卷积核,padding=1,stride=1
输入:256×56×56
输出:256×56×56
再经过ReLu激活函数 - 第三个卷积:256个3×3卷积核,padding=1,stride=1
输入:256×56×56
输出:256×56×56
再经过ReLu激活函数 - 最大池化:2×2,stride=2,padding=0
输入:256×56×56
输出:256×28×28
- 第一个卷积:256个3×3卷积核,padding=1,stride=1
- 第4块:3个conv3-512组成
- 第一个卷积:512个3×3卷积核,padding=1,stride=1
输入:256×28×28
输出:512×28×28
再经过ReLu激活函数 - 第二个卷积:512个3×3卷积核,padding=1,stride=1
输入:512×28×28
输出:512×28×28
再经过ReLu激活函数 - 第三个卷积:512个3×3卷积核,padding=1,stride=1
输入:512×28×28
输出:512×28×28
再经过ReLu激活函数 - 最大池化:2×2,stride=2,padding=0
输入:512×28×28
输出:512×14×14
- 第一个卷积:512个3×3卷积核,padding=1,stride=1
- 第5块:3个conv3-512组成
- 第一个卷积:512个3×3卷积核,padding=1,stride=1
输入:512×14×14
输出:512×14×14
再经过ReLu激活函数 - 第二个卷积:512个3×3卷积核,padding=1,stride=1
输入:512×14×14
输出:512×14×14
再经过ReLu激活函数 - 第三个卷积:512个3×3卷积核,padding=1,stride=1
输入:512×14×14
输出:512×14×14
再经过ReLu激活函数 - 最大池化:2×2,stride=2,padding=0
输入:512×14×14
输出:512×7×7
- 第一个卷积:512个3×3卷积核,padding=1,stride=1
- 全连接层1:先通过nn.Flatten()将512×7×7展开到25088
输入:25088
输出:4096
再经过ReLu激活函数
再经过dropout=0.5 - 全连接层2:
输入:4096
输出:4096
再经过ReLu激活函数
再经过dropout=0.5 - 全连接层3
输入:4096
输出:1000
- 最终输出层:
输入:1000
输出:2
(将 1000 维映射到 2 维,二分类任务:猫和狗)
4️⃣ 代码
1. 分割数据,创建一个名为split_data.py的文件
需要数据集的话请私信我
import os
from shutil import copy
import random
from tqdm import tqdmdef mkfile(file):if not os.path.exists(file):os.makedirs(file)folder_path = "D:\\code\\AI_ladder\\2_VGG"# orig_data_path="D:\\code\\AI_ladder\\2_VGG\\data_origin"
orig_data_path = os.path.join(folder_path + '\\data_origin')
pet_class = [cla for cla in os.listdir(orig_data_path)]mkfile(folder_path + '\\data\\train')
mkfile(folder_path + '\\data\\val')for cla in pet_class:mkfile(folder_path + '\\data\\train\\' + cla)mkfile(folder_path + '\\data\\val\\' + cla)split_rate = 0.2# pet_class=['cat','dog']
for cla in pet_class:# D:\\code\\AI_ladder\\2_VGG\\data_origin\\cat\\# D:\\code\\AI_ladder\\2_VGG\\data_origin\\dog\\cla_path = orig_data_path + '\\' + cla + '\\'# cla_path路径下的所有图片文件名images = os.listdir(cla_path)# 统计数量num = len(images)eval_index = random.sample(images, k=int(num * split_rate))# enumerate(images),它会遍历 images 列表中的每个图片文件,同时生成一个 (index, image) 对,# 其中 index 是当前图片的索引,image 是当前图片的名称# 例如,假设 images = ['img1.jpg', 'img2.jpg', 'img3.jpg'],那么 enumerate(images) 将会生成:# (0, 'img1.jpg')# (1, 'img2.jpg')# (2, 'img3.jpg')# tqdm 接受一个迭代对象作为参数,并为这个迭代对象创建一个进度条显示。# 在这里,tqdm 接收 enumerate(images) 作为迭代对象# tqdm 会自动更新进度条,每次从 enumerate(images) 中获取一个新的 (index, image) 时,进度条会自动增加# total=len(images) 总共需要处理的图片数量,指定进度条的总长度loop = tqdm(enumerate(images),total=len(images))# 设置进度条的前缀描述信息loop.set_description(f'[{cla}]')for index, image in loop:# 验证集if image in eval_index:image_path = cla_path + imagenew_path = folder_path + '\\data\\val\\' + clacopy(image_path, new_path)# 训练集else:image_path = cla_path + imagenew_path = folder_path + '\\data\\train\\' + clacopy(image_path, new_path)print("\nprocessing done")
2.网络结构,创建一个名为network.py的文件
# 导入pytorch库
import torch
# 引入了神经网络模块nn
from torch import nn# 定义VGG类
class VGG(nn.Module):# conv_arch是VGG中每个块的配置参数,包含两部分:卷积层个数和输出通道数def __init__(self, conv_arch):super(VGG, self).__init__() self.conv_arch=conv_arch# 创建VGG块self.conv_blks = self.make_vgg_blocks(conv_arch)# 创建三个全连接层# 先nn.Flatten 将输入展平为一维张量,以便传入线性层# 全连接层1:第一个线性层的输入大小为 25088,输出大小为 4096,再经过ReLu和dropout# 全连接层2:第二个线性层的输入大小为 4096,输出大小为 4096,再经过ReLu和dropout# 全连接层3:第三个线性层的输入大小为 4096,输出大小为 10,# 二分类:最终输出层nn.Linear(1000, 2)self.fc = nn.Sequential(nn.Flatten(),nn.Linear(25088, 4096),nn.ReLU(),nn.Dropout(0.5),nn.Linear(4096, 4096),nn.ReLU(),nn.Dropout(0.5),nn.Linear(4096, 1000),nn.Linear(1000, 2))def make_vgg_blocks(self, conv_arch):# 定义了一个空列表blocks,用于存储生成的VGG块blocks = []# 输入通道维度in_channels = 3# 遍历 conv_arch# num_convs:卷积层个数# out_channels:输出通道数for (num_convs, out_channels) in conv_arch:# 创建VGG块blocks.append(self.vgg_block(num_convs, in_channels, out_channels))# 在块与块之间,下一个块的输入通道数等于上一个块的输出通道数in_channels = out_channelsreturn nn.Sequential(*blocks)# 静态方法,调用时不需要传递 self 参数,也就是说,这个方法不会直接访问实例属性@staticmethoddef vgg_block(num_convs, in_channels, out_channels):# 定义了一个空列表layers,用于存储VGG块中的所有层layers = []# 遍历 卷积层个数for _ in range(num_convs):# VGG中所有的卷积核的大小都是3×3,padding=1,stride=1layers.append(nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=3, padding=1))# 激活函数layers.append(nn.ReLU())# 在同一个块中,下一层卷积的输入通道数等于上一层输出的卷积通道数in_channels = out_channels# 最大池化,2×2,stride=2,padding=0layers.append(nn.MaxPool2d(kernel_size=2, stride=2))# 使用星号 * 对列表进行解包,将 layers 中的每个元素作为 nn.Sequential 的参数传递return nn.Sequential(*layers)def forward(self, x):feature = self.conv_blks(x)output = self.fc(feature)return outputdef register_hooks(model):# 定义钩子函数 hook:这是一个内部函数,它会被注册为模型每一层的钩子。# 在前向传播过程中,当每一层执行完毕时,这个钩子函数会被触发# module:表示当前被钩子的模块(层)。例如,它可以是卷积层、池化层、全连接层等。# input:是传入当前模块的输入张量,形状与输入数据的形状一致(通常是元组形式)。# output:是从当前模块得到的输出张量def hook(module, input, output):# 获取当前层的类名,例如 Conv2d、ReLU、MaxPool2d 等class_name = module.__class__.__name__# 打印当前层的类名和其输出张量的形状print(f"{class_name} Output Shape: {output.shape}")# 注册钩子函数for layer in model.modules():# 排除 nn.Sequential:nn.Sequential 是一个包含多个层的容器类,我们通常希望钩子直接注册到具体的层,而不是容器本身。# 排除 nn.ModuleList:nn.ModuleList 是另一种容器类,通常用于将多个模块按列表存储。# 排除顶级的模型类本身:检查 (layer == model) 以确保不会在整个模型对象上注册钩子if not isinstance(layer, nn.Sequential) and not isinstance(layer, nn.ModuleList) and not (layer == model):# 对于通过检查的每一个 layer,调用 register_forward_hook 方法,将之前定义的 hook 函数注册到该层上layer.register_forward_hook(hook)if __name__=='__main__':# conv_arch是VGG中每个块的配置参数,包含两部分:卷积层个数和输出通道数# VGG中的卷积包含padding,因此不改变高宽,卷积的作用是改变输出通道数# 对于VGG16来说,由5个块组成# 第1块:2个conv3-64组成(con3-64中3代表卷积核大小为3×3,64代表64个卷积核)# 第2块:2个conv3-128组成# 第3块:3个conv3-256组成# 第4块:3个conv3-512组成# 第5块:3个conv3-512组成conv_arch = ((2, 64), (2, 128), (3, 256), (3, 512), (3, 512))# 创建输入张量 X,其大小为 (1, 3, 224, 224),表示批次大小为 1、三通道、尺寸为 224x224X = torch.randn(size=(1, 3, 224, 224))# 实例化 VGG 模型 netnet = VGG(conv_arch)# 注册钩子以打印每层的输出尺寸register_hooks(net)# 进行一次前向传播,输出最终结果y = net(X)print('Final output:\t', y)
这里打印了每一层的输出大小,和前面通过公式计算得到的结果一致:
输入[1,3,224,224]
第一个VGG块
Conv2d Output Shape: torch.Size([1, 64, 224, 224])
ReLU Output Shape: torch.Size([1, 64, 224, 224])
Conv2d Output Shape: torch.Size([1, 64, 224, 224])
ReLU Output Shape: torch.Size([1, 64, 224, 224])
MaxPool2d Output Shape: torch.Size([1, 64, 112, 112])
第二个VGG块
Conv2d Output Shape: torch.Size([1, 128, 112, 112])
ReLU Output Shape: torch.Size([1, 128, 112, 112])
Conv2d Output Shape: torch.Size([1, 128, 112, 112])
ReLU Output Shape: torch.Size([1, 128, 112, 112])
MaxPool2d Output Shape: torch.Size([1, 128, 56, 56])
第三个VGG块
Conv2d Output Shape: torch.Size([1, 256, 56, 56])
ReLU Output Shape: torch.Size([1, 256, 56, 56])
Conv2d Output Shape: torch.Size([1, 256, 56, 56])
ReLU Output Shape: torch.Size([1, 256, 56, 56])
Conv2d Output Shape: torch.Size([1, 256, 56, 56])
ReLU Output Shape: torch.Size([1, 256, 56, 56])
MaxPool2d Output Shape: torch.Size([1, 256, 28, 28])
第四个VGG块
Conv2d Output Shape: torch.Size([1, 512, 28, 28])
ReLU Output Shape: torch.Size([1, 512, 28, 28])
Conv2d Output Shape: torch.Size([1, 512, 28, 28])
ReLU Output Shape: torch.Size([1, 512, 28, 28])
Conv2d Output Shape: torch.Size([1, 512, 28, 28])
ReLU Output Shape: torch.Size([1, 512, 28, 28])
MaxPool2d Output Shape: torch.Size([1, 512, 14, 14])
第五个VGG块
Conv2d Output Shape: torch.Size([1, 512, 14, 14])
ReLU Output Shape: torch.Size([1, 512, 14, 14])
Conv2d Output Shape: torch.Size([1, 512, 14, 14])
ReLU Output Shape: torch.Size([1, 512, 14, 14])
Conv2d Output Shape: torch.Size([1, 512, 14, 14])
ReLU Output Shape: torch.Size([1, 512, 14, 14])
MaxPool2d Output Shape: torch.Size([1, 512, 7, 7])
全连接层1
Flatten Output Shape: torch.Size([1, 25088])
Linear Output Shape: torch.Size([1, 4096])
ReLU Output Shape: torch.Size([1, 4096])
Dropout Output Shape: torch.Size([1, 4096])
全连接层2
Linear Output Shape: torch.Size([1, 4096])
ReLU Output Shape: torch.Size([1, 4096])
Dropout Output Shape: torch.Size([1, 4096])
全连接层3
Linear Output Shape: torch.Size([1, 1000])
最终输出层
Linear Output Shape: torch.Size([1, 2])
3.训练网络,创建一个名为train.py的文件
# 导入pytorch库
import torch
# 引入了神经网络模块nn
from torch import nn# 定义VGG类
class VGG(nn.Module):# conv_arch是VGG中每个块的配置参数,包含两部分:卷积层个数和输出通道数def __init__(self, conv_arch):super(VGG, self).__init__() self.conv_arch=conv_arch# 创建VGG块self.conv_blks = self.make_vgg_blocks(conv_arch)# 创建三个全连接层# 先nn.Flatten 将输入展平为一维张量,以便传入线性层# 全连接层1:第一个线性层的输入大小为 25088,输出大小为 4096,再经过ReLu和dropout# 全连接层2:第二个线性层的输入大小为 4096,输出大小为 4096,再经过ReLu和dropout# 全连接层3:第三个线性层的输入大小为 4096,输出大小为 10,# 二分类:最终输出层nn.Linear(1000, 2)self.fc = nn.Sequential(nn.Flatten(),nn.Linear(25088, 4096),nn.ReLU(),nn.Dropout(0.5),nn.Linear(4096, 4096),nn.ReLU(),nn.Dropout(0.5),nn.Linear(4096, 1000),nn.Linear(1000, 2))def make_vgg_blocks(self, conv_arch):# 定义了一个空列表blocks,用于存储生成的VGG块blocks = []# 输入通道维度in_channels = 3# 遍历 conv_arch# num_convs:卷积层个数# out_channels:输出通道数for (num_convs, out_channels) in conv_arch:# 创建VGG块blocks.append(self.vgg_block(num_convs, in_channels, out_channels))# 在块与块之间,下一个块的输入通道数等于上一个块的输出通道数in_channels = out_channelsreturn nn.Sequential(*blocks)# 静态方法,调用时不需要传递 self 参数,也就是说,这个方法不会直接访问实例属性@staticmethoddef vgg_block(num_convs, in_channels, out_channels):# 定义了一个空列表layers,用于存储VGG块中的所有层layers = []# 遍历 卷积层个数for _ in range(num_convs):# VGG中所有的卷积核的大小都是3×3,padding=1,stride=1layers.append(nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=3, padding=1))# 激活函数layers.append(nn.ReLU())# 在同一个块中,下一层卷积的输入通道数等于上一层输出的卷积通道数in_channels = out_channels# 最大池化,2×2,stride=2,padding=0layers.append(nn.MaxPool2d(kernel_size=2, stride=2))# 使用星号 * 对列表进行解包,将 layers 中的每个元素作为 nn.Sequential 的参数传递return nn.Sequential(*layers)def forward(self, x):feature = self.conv_blks(x)output = self.fc(feature)return outputdef register_hooks(model):# 定义钩子函数 hook:这是一个内部函数,它会被注册为模型每一层的钩子。# 在前向传播过程中,当每一层执行完毕时,这个钩子函数会被触发# module:表示当前被钩子的模块(层)。例如,它可以是卷积层、池化层、全连接层等。# input:是传入当前模块的输入张量,形状与输入数据的形状一致(通常是元组形式)。# output:是从当前模块得到的输出张量def hook(module, input, output):# 获取当前层的类名,例如 Conv2d、ReLU、MaxPool2d 等class_name = module.__class__.__name__# 打印当前层的类名和其输出张量的形状print(f"{class_name} Output Shape: {output.shape}")# 注册钩子函数for layer in model.modules():# 排除 nn.Sequential:nn.Sequential 是一个包含多个层的容器类,我们通常希望钩子直接注册到具体的层,而不是容器本身。# 排除 nn.ModuleList:nn.ModuleList 是另一种容器类,通常用于将多个模块按列表存储。# 排除顶级的模型类本身:检查 (layer == model) 以确保不会在整个模型对象上注册钩子if not isinstance(layer, nn.Sequential) and not isinstance(layer, nn.ModuleList) and not (layer == model):# 对于通过检查的每一个 layer,调用 register_forward_hook 方法,将之前定义的 hook 函数注册到该层上layer.register_forward_hook(hook)if __name__=='__main__':# conv_arch是VGG中每个块的配置参数,包含两部分:卷积层个数和输出通道数# VGG中的卷积包含padding,因此不改变高宽,卷积的作用是改变输出通道数# 对于VGG16来说,由5个块组成# 第1块:2个conv3-64组成(con3-64中3代表卷积核大小为3×3,64代表64个卷积核)# 第2块:2个conv3-128组成# 第3块:3个conv3-256组成# 第4块:3个conv3-512组成# 第5块:3个conv3-512组成conv_arch = ((2, 64), (2, 128), (3, 256), (3, 512), (3, 512))# 创建输入张量 X,其大小为 (1, 3, 224, 224),表示批次大小为 1、三通道、尺寸为 224x224X = torch.randn(size=(1, 3, 224, 224))# 实例化 VGG 模型 netnet = VGG(conv_arch)# 注册钩子以打印每层的输出尺寸register_hooks(net)# 进行一次前向传播,输出最终结果y = net(X)print('Final output:\t', y)