一. VGG 神经网络介绍
VGG-16 是一种深度卷积神经网络(CNN),由牛津大学视觉几何组(Visual Geometry Group,简称VGG)的 Karen Simonyan和Andrew Zisserman 在 2014 年提出。VGG-16 因其结构的简洁性和一致性而著名,它主要由堆叠的小型 3x3 卷积核和 2x2 的最大池化层构成。VGG-16 在 ILSVRC 2014 比赛中获得了图像分类任务的亚军和定位任务的冠军。
二. 概念拓展
感受野(receptive field)
- 在卷积神经网络中,决定某一层输出结果中一个元素所对应的输入层的区域大小,被称作感受野。通俗的来说就是,输出 feature map 上的一个单元对应输入层上的区域大小。
简单示例
如上图,最下层是一个 9 ∗ 9 ∗ 1 9*9*1 9∗9∗1 的特征矩阵 ,首先将其通过 Conv1(大小为 3 ∗ 3 3*3 3∗3,步距为 2),通过计算公式,可以得到大小为 4 ∗ 4 ∗ 1 4*4*1 4∗4∗1 的特征矩阵;再将其通过最大池化下采样操作(大小为 2 ∗ 2 2*2 2∗2,步距为 2),得到一个 2 ∗ 2 ∗ 1 2*2*1 2∗2∗1 的特征矩阵。
计算感受野
感受野计算公式为:
F
(
i
)
=
(
F
(
i
+
1
)
−
1
)
×
S
t
r
i
d
e
+
K
s
i
z
e
F(i)=(F(i+1)-1)\times Stride + Ksize
F(i)=(F(i+1)−1)×Stride+Ksize
其中,
F
(
i
)
F(i)
F(i) 为第
i
i
i层感受野,
S
t
r
i
d
e
Stride
Stride 为第
i
i
i 层的步距,
K
s
i
z
e
Ksize
Ksize 为卷积核或池化核尺寸。
- F e a t u r e m a p Feature map Featuremap (最后得到的特征图): F ( 3 ) = 1 F(3)=1 F(3)=1
- M a x P o o l 1 MaxPool1 MaxPool1 层:其输出的是 2 ∗ 2 2*2 2∗2 大小,其输入的是 4 ∗ 4 4*4 4∗4 大小, K s i z e = 2 Ksize=2 Ksize=2, S t r i d e = 2 Stride=2 Stride=2 则: F ( 2 ) = ( F ( 3 ) − 1 ) ∗ 2 + 2 = ( 1 − 1 ) ∗ 2 + 2 = 2 F(2)=(F(3)-1)*2+2=(1-1)*2+2=2 F(2)=(F(3)−1)∗2+2=(1−1)∗2+2=2
- C o n v 1 Conv1 Conv1:其输出的是 4 ∗ 4 4*4 4∗4 大小,其输入的是 9 ∗ 9 9*9 9∗9 大小, K s i z e = 3 Ksize=3 Ksize=3, S t r i d e = 2 Stride=2 Stride=2 则: F ( 1 ) = ( F ( 2 ) − 1 ) ∗ 2 + 3 = ( 2 − 1 ) ∗ 2 + 3 = 5 F(1)=(F(2)-1)*2+3=(2-1)*2+3=5 F(1)=(F(2)−1)∗2+3=(2−1)∗2+3=5
三. VGG 神经网络结构
VGG-16 结构概述
我们常用的是 D 配置,即 VGG-16,如上图,VGG-16 的 “16” 来源于网络中含有的 16 个有可学习权重的层,其中包括 13 个卷积层和 3 个全连接层。所有的卷积层都使用 3x3 大小的卷积核,所有的池化层都使用 2x2 的窗口和步幅为 2,这样可以将特征图的尺寸减半。
具体结构
VGG-16 网络结构可以被划分为5个主要的区块(block),每个区块通常由多个卷积层和一个池化层组成。下面是每个区块的详细配置:
-
Block 1
- 两个 3x3 的卷积层,每个层有 64 个滤波器,步幅为 1, padding 为 1。
- 一个 2x2 的最大池化层,步幅为 2。
-
Block 2
- 两个 3x3 的卷积层,每个层有 128 个滤波器,步幅为 1, padding 为 1。
- 一个 2x2 的最大池化层,步幅为 2。
-
Block 3
- 三个 3x3 的卷积层,每个层有 256 个滤波器,步幅为 1, padding 为 1。
- 一个 2x2 的最大池化层,步幅为 2。
-
Block 4
- 三个 3x3 的卷积层,每个层有 512 个滤波器,步幅为 1, padding 为 1。
- 一个 2x2 的最大池化层,步幅为 2。
-
Block 5
- 三个 3x3 的卷积层,每个层有 512 个滤波器,步幅为 1, padding 为 1。
- 一个 2x2 的最大池化层,步幅为 2。
在最后一个池化层之后,网络进入全连接层阶段:
- 第一个全连接层有 4096 个神经元。
- 第二个全连接层也有 4096 个神经元。
- 最后一个全连接层是输出层,具有与分类任务类别数量相同的神经元(例如,在 ImageNet 任务中,这是 1000 个类别)。
在每个卷积层和全连接层之后,通常会跟随 ReLU 激活函数,以引入非线性特性。
输入和输出
VGG-16 网络期望的输入图像尺寸是 224x224 像素,通常输入图像会被预处理,包括归一化和可能的均值减除。
参数量
VGG-16 网络大约有 1.38 亿个可训练参数,这使得它在计算资源和存储方面要求较高。
四. VGG 模型亮点
VGG 模型,全称为 Visual Geometry Group model,是由牛津大学视觉几何组提出的深度学习模型之一。VGG 模型在深度学习领域内因其简单的架构和卓越的性能而受到广泛的关注和应用。以下是 VGG 模型的一些亮点:
-
标准化结构:
VGG 模型采用了一种非常规整的网络结构,几乎完全由 3x3 的卷积层和 2x2 的最大池化层构成。这种标准化的设计简化了模型的搭建和理解,同时也便于调整网络深度。 -
小卷积核的堆叠:
VGG 模型大量使用了 3x3 的卷积核,并通过堆叠这些小卷积核来构建更大感受野,这相比于使用单个大卷积核(如 7x7)减少了参数的数量,提高了计算效率,并且增强了模型的非线性表达能力。(堆叠两个 3x3 的卷积核替代 5x5 的卷积核,堆叠三个 3x3 的卷积核替代 7x7 的卷积核,拥有相同的感受野。) -
深度网络的验证:
VGG 模型验证了在图像识别任务中,增加网络深度可以显著提高模型的性能。VGG 模型有多个版本,其中最著名的 VGG-16 和 VGG-19 分别有 16 和 19 个权重层,这在当时是非常深的网络。 -
参数量:
尽管 VGG 模型的参数量较大(约 1.38 亿个参数),但这种规模化的参数量是通过小卷积核的堆叠来实现的,这相比于同等深度的网络使用大卷积核时所需的参数要少得多。 -
ReLU 激活函数和 Dropout:
VGG 模型使用 ReLU 作为激活函数,这有助于加速训练过程并避免梯度消失问题。此外,VGG 模型在全连接层中使用 Dropout 技术,以防止过拟合,提高模型的泛化能力。 -
优秀的泛化能力:
VGG 模型不仅在 ImageNet 数据集上表现出色,还被广泛应用于许多其他视觉任务和数据集上,显示出了良好的泛化性能。 -
模块化设计:
VGG 模型可以被视为由多个模块或“VGG块”组成,每个块包含多个连续的卷积层和一个池化层,这种模块化的设计使得模型易于扩展和修改。 -
图像分类基准:
VGG 模型成为了深度学习领域内图像分类任务的一个重要基准,许多后续的研究工作都会将其作为对比的基线模型。
五. VGG代码实现
开发环境配置说明:本项目使用 Python 3.6.13 和 PyTorch 1.10.2 构建,适用于CPU环境。
- model.py:定义网络模型
- train.py:加载数据集并训练,计算 loss 和 accuracy,保存训练好的网络参数
- predict.py:用自己的数据集进行分类测试
- model.py
import torch.nn as nn
import torch
# official pretrain weights
model_urls = {
'vgg11': 'https://download.pytorch.org/models/vgg11-bbd30ac9.pth',
'vgg13': 'https://download.pytorch.org/models/vgg13-c768596a.pth',
'vgg16': 'https://download.pytorch.org/models/vgg16-397923af.pth',
'vgg19': 'https://download.pytorch.org/models/vgg19-dcbb9e9d.pth'
}
class VGG(nn.Module):
def __init__(self, features, class_num=1000, init_weights=False):
super(VGG, self).__init__()
# 初始化特征提取网络
self.features = features
# 初始化全连接层
self.classifier = nn.Sequential(
nn.Dropout(p=0.5),
nn.Linear(512 * 7 * 7, 2048),
nn.ReLU(True),
nn.Dropout(p=0.5),
nn.Linear(2048, 2048),
nn.ReLU(True),
nn.Linear(2048, class_num)
)
if init_weights:
self._initialize_weights()
def forward(self, x):
# N x 3 x 224 x 224
x = self.features(x)
# N x 512 x 7 x 7
x = torch.flatten(x, start_dim=1)
# N x 512*7*7
x = self.classifier(x)
return x
def _initialize_weights(self):
for m in self.modules():
if isinstance(m, nn.Conv2d):
# nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
nn.init.xavier_uniform_(m.weight)
if m.bias is not None:
nn.init.constant_(m.bias, 0)
elif isinstance(m, nn.Linear):
nn.init.xavier_uniform_(m.weight)
# nn.init.normal_(m.weight, 0, 0.01)
nn.init.constant_(m.bias, 0)
# 特征提取网络构造函数 根据传入对应配置的cfg列表,构造对应的提取网络
def make_features(cfg:list):
# 定义空列表,用来存放创建的每一层结构
layers = []
# 初始化输入通道数(RGB图像)
in_channels = 3
# 遍历配置列表
for v in cfg:
# 如果当前配置元素是"M"字符
if v == "M":
# 创建最大池化下采样层
layers += [nn.MaxPool2d(kernel_size=2, stride=2)]
# 否则
else:
# 创建卷积操作
conv2d = nn.Conv2d(in_channels, v, kernel_size=3, padding=1)
# 将定义好的卷积层与激活函数拼接在一起,添加到layers列表中
layers += [conv2d, nn.ReLU(True)]
# 更新通道数
in_channels = v
# 通过非关键字参数*layers,将特征提取网络输入nn.Sequential()进行整合
return nn.Sequential(*layers)
# 定义模型配置的字典文件
cfgs = {
'vgg11': [64, 'M', 128, 'M', 256, 256, 'M', 512, 512, 'M', 512, 512, 'M'],
'vgg13': [64, 64, 'M', 128, 128, 'M', 256, 256, 'M', 512, 512, 'M', 512, 512, 'M'],
'vgg16': [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 'M', 512, 512, 512, 'M', 512, 512, 512, 'M'],
'vgg19': [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 256, 'M', 512, 512, 512, 512, 'M', 512, 512, 512, 512, 'M'],
}
def vgg(model_name="vgg16", **kwargs):
try:
cfg = cfgs[model_name]
except:
print("Warning: model number {} not in cfgs dict!".format(model_name))
exit(-1)
# 实例化模型 **kwargs 代表可变长度的字典变量
model = VGG(make_features(cfg), **kwargs)
return model
- train.py
import torch
import torch.nn as nn
from torchvision import transforms, datasets
from torch.utils.data import DataLoader
import torch.optim as optim
from model import vgg
import os
import json
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
# print(device)
data_transform = {
"train" : transforms.Compose([transforms.RandomResizedCrop(224), # 随机裁剪
transforms.RandomHorizontalFlip(), # 随机翻转
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5),(0.5, 0.5, 0.5))]),
"val" : transforms.Compose([transforms.Resize((224, 224)), # 不能224,必须(224, 224)
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5),(0.5, 0.5, 0.5))])}
# 获取数据集所在的根目录
# 通过os.getcwd()获取当前的目录,并将当前目录与".."链接获取上一层目录
data_root = os.path.abspath(os.path.join(os.getcwd(), ".."))
# 获取花类数据集路径
image_path = data_root + "/data_set/flower_data/"
# 加载数据集
train_dataset = datasets.ImageFolder(root=image_path + "/train",
transform=data_transform["train"])
# 获取训练集图像数量
train_num = len(train_dataset)
# 获取分类的名称
# {'daisy': 0, 'dandelion': 1, 'roses': 2, 'sunflowers': 3, 'tulips': 4}
flower_list = train_dataset.class_to_idx
# 采用遍历方法,将分类名称的key与value反过来
cla_dict = dict((val, key) for key, val in flower_list.items())
# 将字典cla_dict编码为json格式
json_str = json.dumps(cla_dict, indent=4)
with open("class_indices.json", "w") as json_file:
json_file.write(json_str)
batch_size = 32
train_loader = DataLoader(train_dataset,
batch_size=batch_size,
shuffle=True,
num_workers=0)
validate_dataset = datasets.ImageFolder(root=image_path + "/val",
transform=data_transform["val"])
val_num = len(validate_dataset)
validate_loader = DataLoader(validate_dataset,
batch_size=batch_size,
shuffle=True,
num_workers=0)
model_name = "vgg11"
net = vgg(model_name=model_name, class_num=5, init_weights=True) # 实例化模型
net.to(device)
loss_function = nn.CrossEntropyLoss() # 定义损失函数
#pata = list(net.parameters()) # 查看模型参数
optimizer = optim.Adam(net.parameters(), lr=0.0001) # 定义优化器
# 设置存储权重路径
save_path = './{}Net.pth'.format(model_name)
best_acc = 0.0
for epoch in range(1):
# train
net.train() # 用来管理Dropout方法:训练时使用Dropout方法,验证时不使用Dropout方法
running_loss = 0.0 # 用来累加训练中的损失
for step, data in enumerate(train_loader, start=0):
# 获取数据的图像和标签
images, labels = data
# 将历史损失梯度清零
optimizer.zero_grad()
# 参数更新
outputs = net(images.to(device)) # 获得网络输出
loss = loss_function(outputs, labels.to(device)) # 计算loss
loss.backward() # 误差反向传播
optimizer.step() # 更新节点参数
# 打印统计信息
running_loss += loss.item()
# 打印训练进度
rate = (step + 1) / len(train_loader)
a = "*" * int(rate * 50)
b = "." * int((1 - rate) * 50)
print("\rtrain loss: {:^3.0f}%[{}->{}]{:.3f}".format(int(rate * 100), a, b, loss), end="")
print()
# validate
net.eval() # 关闭Dropout方法
acc = 0.0
# 验证过程中不计算损失梯度
with torch.no_grad():
for data_test in validate_loader:
test_images, test_labels = data_test
outputs = net(test_images.to(device))
predict_y = torch.max(outputs, dim=1)[1]
# acc用来累计验证集中预测正确的数量
# 对比预测值与真实标签,sum()求出预测正确的累加值,item()获取累加值
acc += (predict_y == test_labels.to(device)).sum().item()
accurate_test = acc / val_num
# 如果当前准确率大于历史最优准确率
if accurate_test > best_acc:
# 更新历史最优准确率
best_acc = accurate_test
# 保存当前权重
torch.save(net.state_dict(), save_path)
# 打印相应信息
print("[epoch %d] train_loss: %.3f test_accuracy: %.3f"%
(epoch + 1, running_loss / step, acc / val_num))
print("Finished Training")
- predict.py
import torch
from model import vgg
from PIL import Image
from torchvision import transforms
import matplotlib.pyplot as plt
import json
data_transform = transforms.Compose(
[transforms.Resize((224, 224)),
transforms.ToTensor()])
# 载入图像
img = Image.open("./郁金香.png")
# [N, C, H, W]
# 图像预处理
img = data_transform(img)
# 增加 batch 维度
img = torch.unsqueeze(img, dim=0)
# 读取 class_indict
try:
json_file = open("./class_indices.json", "r")
class_indict = json.load(json_file)
except Exception as e:
print(e)
exit(-1)
# 创建模型
model = vgg(model_name="vgg11", class_num=5)
# 加载权重
model_weight_path = "./vgg11Net.pth"
model.load_state_dict(torch.load(model_weight_path))
# 屏蔽Dropout
model.eval()
with torch.no_grad():
# model(img)将图像输入模型得到输出,采用squeeze压缩维度,即将Batch维度压缩掉
output = torch.squeeze(model(img))
# 采用softmax将最终输出转化为概率分布
predict = torch.softmax(output, dim=0)
# 获取概率最大处的索引值
predict_cla = torch.argmax(predict).numpy()
# 打印类别名称及其对应的预测概率
print(class_indict[str(predict_cla)], predict[predict_cla].item())