目录
1. VGG解决的问题
2. 网络结构和参数
3. pytorch搭建vgg
4.flower_photos分类任务实践
5.资料
一、VGG解决的问题
论文链接:https://arxiv.org/pdf/1409.1556
在VGG之前,大多数深度学习模型相对较浅,比如下面的AlexNet(5层卷积和3层全连接层)。
Vgg网络的一个亮点是,通过使用小尺寸卷积核,有效的增量网络的深度,降低计算的复杂度。堆叠多个3×3的卷积核来替代大尺度卷积核,减少所需参数:通过堆叠2个3×3的卷积核替代5×5的卷积核,堆叠3个3×3的卷积核替代7×7的卷积核,两者之间拥有相同的感受野
感受野(Receptive Field)是指输出特征图(feature map)的一个元素 对应输入图像上区域大小,较大的感受野可以帮助网络捕获更多的上下文信息,较小的感受野更适合捕捉图像中的细节特征。
感受野计算公式:F(i)=[F(i+1)-1]×Stride + Ksize
eg:步长Stride=1,卷积核大小Ksize=3
Feature map: F = 1
Conv3x3(3): F = (1 - 1) x 1 + 3 = 3
Conv3x3(2): F = (3 - 1) x 1 + 3 = 5 #堆叠2个3×3的卷积核替代5×5的卷积核
Conv3x3(1): F = (5 - 1) x 1 + 3 = 7 #堆叠3个3×3的卷积核替代7×7的卷积核
使用7x7的卷积核所需参数为7x7xCxC = 49C²,使用3个3x3卷积核3x(3x3xCxC) = 27C²。(第一个C表示输入特征矩阵深度,第二个C表示卷积核个数即输出矩阵深度),使用多个小卷积核进行堆叠比使用大卷积核 在相同感受野的情况下,使用的参数量更少
图像来自:VGG16网络结构详解
VGG另一个亮点是:使用了统一的结构,多个连续的卷积层,后面跟Relu,然后进行最大池化层处理。虽然之前的网络如Alexnet也是这种结构,但是它在不同层的卷积核大小和数量以及步长等参数有较大的变化,而VGG不同层的参数基本一致,后续的Resnet也是采用这种重复性的结构,便于扩展。
二、网络结构和参数
图片来自:https://arxiv.org/pdf/1409.1556
VGG16的结构对应如下
由于VGG最初用于ImageNet数据集的分类任务,所以输入图片统一尺寸为(224, 224)。整个网络中都用了3x3大小的卷积核,并配合1x1的卷积核进行简单的线型转变(不改变图片大小)
三、pytorch搭建Vgg(代码加了详细注释)
3.1 网络模型搭建
"""
VGG模型
"""
import torch
import torch.nn as nn
class VGG(nn.Module):
def __init__(self,features,num_classes=1000):
#VGG 是当前类对象, self是当前实例对象
#传递类对象让父类知道哪个子类在被初始化;传递实例对象为了让父类的构造方法能够访问和修改子类的实例
super(VGG,self).__init__()
#定义特征提取网络
self.features = features
#定义分类网络
self.classifier = nn.Sequential(
#将特征提取网络的卷积层输出的特征图(7X7大小,512个通道)展平flatten为4096的一维向量,应用全链接层
nn.Linear(7*7*512,4096),
#引入非线性激活Relu函数,inplace为True在某些情况可以节省内存
nn.ReLU(True),
#dropout正则化,已0.5的概率随机丢弃(置零)一些神经元,防止过拟合
nn.Dropout(p=0.5),
#同上,这里将4096维输入映射为另外一个4096维空间.增加非线性能力,特征进行转换,提取更有用信息
nn.Linear(4096,4096)
nn.ReLU(True),
nn.Dropout(p=0.5),
#第三层全链接层,将4096维映射为类别数量的维度,为每个类型分别生成一个实数得分
nn.Linear(4096,num_classes),
)
#前向传播, 传入收入u的图片
def forward(self,x):
#特征提取
x=self.features(x)
#特征提取的输出是四维张量(batchsize,channel,h,w),这里start_dim=1沿第一维进行展平,结果为(batchsize,channel*w*h). pytorch中全链接层(nn.Linear)期望的收入u是一个二维张量,其中第一维为batchsize,第二维为样本的特征向量.因此需要手动的将卷积层输出的多维数据展平为二维
x = torch.flatten(x,start_dim=1)
#分类
x = self.classifier(x)
return x
# 字典配置,可以很方便的切换不同深度的网络
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 make_features(cfg:list):
#用于存储网络层列表
layers =[]
#输入的图像通道RGB
in_channels = 3
#遍历列表,根据配置 实现不同的layer
for v in cfg:
#最大池化层
if v=='M':
layer +=[nn.MaxPool2d(kernel_size=2,stride=2)]
else:
#构建2d卷积层以及紧跟的relu激活函数
conv2d = nn.Conv2d(in_channel,v,kernel_size=3,padding=1)
layer += [conv2d,nn.ReLU(True)]
#把当前输出作为下一层的输入
in_channels = v
#最后使用Sequential把layer连接起来,这里使用了列表非关键词解包
return nn.Sequential(*layers)
def vgg(model_name =='vgg16',**kwargs):
#根据model_name从配置中获取对应的网络结构
cfg = cfgs[model_name]
#提取特征
fetures = make_features(cfg)
#根据特征,以及非关键词参数创建VGG模型对象
model=VGG(features,**kwargs)
return model
3.2 模型训练
"""
VGG 训练代码
"""
import os
import sys
import torch
import torch.nn as nn
from torchvision import transforms,datasets,utils
import matplotlib.pyplot as plt
import torch.optim as optim
from tqdm import tqdm
from model import VGG
def train():
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
#数据增强
data_transform = {
#训练数据,采用随机尺寸裁剪和随机水平翻转来增加数据的多样性
#toTensor 将PIL或者numpy ndarray转为FloatTensor,并吧图像的像素之从0-255转为0-1
#归一化使得模型的训练更加稳定
"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)),
transforms.ToTensor(),
transforms.Normalize((0.5,0.5,0.5),(0.5,0.5,0.5))
])
}
data_root = os.path.abspath(os.path.join(os.getcwd(), "./"))
image_path = os.path.join(data_root, "data_set")
#加载数据集,datasets.ImageFolder类 用于加载按照分类命名的文件夹结构数据(每个类一个文件夹)
train_dataset = datasets.ImageFolder(root=os.path.join(image_path,"train"),
transform = data_transform['train']
)
val_dataset = datasets.ImageFolder(root=os.path.join(image_path,"val"),
transform = data_transform['val']
)
#初始化数据加载器, 负责将数据集分批次处理,对于内存管理和训练加速很重要
train_loader = torch.utils.data.DataLoader(train_dataset,
batch_size = 32,
shuffle=True,
num_workers=0
)
val_loader = torch.utils.data.DataLoader(val_dataset,
batch_size = 32,
shuffle=True,
num_workers=0
)
#初始化网络 loss函数以及优化器
model_name = "vgg16"
net = vgg(model_name=model_name,num_classes=5)
net.to(device)
#使用分类任务常用的交叉损失熵loss; 如果是回归任务常用的是MSEloss;如果是目标检测(更综合的任务),涉及物体的定位和类别识别(一个边界框和类别),会使用多个loss(边界框回归损失;类别分类损失;对象置信度损失)
loss_function = nn.CrossEntropyLoss()
#常用的Adam优化器,Adam优化器结合了动量Momentum和RMSprop,能为不同的参数自适应的调整学习率. 除了Adam也有其他的优化器,比如SGD(随机梯度下降)+Momentum,Adagrad 等
optimizer = optim.Adam(net.parameters(),lr=0.0001)
#训练
epochs=10
save_path = 'VGGNet.pth'
best_accuracy = 0.0
val_num = len(val_dataset)
for epoch in range(epochs):
#设置为训练模式,开始训练
#训练模式会启动所有层的参数更新,在反向传播过程中,梯度会计算并更新到参数上
net.train()
running_loss = 0.0
train_bar = tqdm(train_loader,file=sys.stdout)
for step ,data in enumerate(train_bar):
images,labels = data
#清空梯度
# pytorch中梯度默认是累积的,即如果在同一个优化器实例上连续多次调用backward,梯度将会累加到之前的梯度上,会导致训练不稳定
#每次参数更新应该基于当前批次的损失计算,不应该受到之前批次的影响,保证独立. 清空梯度是优化器的基本要求
optimizer.zero_grad()
#将images转移到deivce设备,前向传播计算输出
outputs = net(images.to(device))
#计算损失
loss = loss_function(outputs,labels.to(device))
#反向传播,负责计算每个参数的梯度,并保存在参数的.grad中但此时参数尚未更新
loss.backward()
#optimizer.step 更新权重,负责计算反向传播得到的梯度来更新模型参数
optimizer.step()
running_loss += loss.item()
#val 在每一轮训练结束后,模型被设置为验证模式,并计算在验证集上的准确率
#(验证)评估模式,禁用所有层的参数更新,即在反向传播过程中,梯度不会计算也不会影响参数;关闭Dropout
net.eval()
acc = 0.0
#torch.no_grad 确保在验证过程中不计算梯度,以节省内存和计算资源
with torch.no_grad():
val_bar = tqdm(val_loader,file=sys.stdout)
for val_data in val_bar:
val_images,val_labels = val_data
outputs = net(val_images.to(device))
predict_y = torch.max(outputs,dim=1)[1]
acc += torch.eq(predict_y,val_labels.to(device)).sum().item()
val_accuracy = acc/val_num
#保存最佳模型
if val_accuracy > best_accuracy:
best_accuracy = val_accuracy
torch.save(net.state_dict(),save_path)
3.3 模型推理
"""
VGG 推理
"""
import os
import torch
from PIL import Image
from torchvision import transforms
from model import vgg
def predict():
device = torch.device('cuda' if torch.cuda.is_available() else "cpu")
data_trasform = transforms.Compose([
transforms.Resize((224,224)),
transforms.ToTensor(),
transforms.Normalize((0.5,0.5,0.5),(0.5,0.5,0.5))
])
image_path = './test.jpg'
img = Image.open(image_path)
img = data_transform(img) #[C H W]
img =torch.unsqueeze(img,dim=0) #维度扩展,在第一维 增加Batchsize维度 [B,C,H,W]
mode = vgg(model_name='vgg16',num_classes=5)
mode = mode.to(device)
weights_path = './VGGNet.pth'
mode.load_state_dict(torch.load(weights_path))
#将模型设置为评估模式,禁用模型中的dropout和batchnormalization的动态行为
mode.eval()
#禁用季度计算,减少内存和计算
with torch.no_grad():
output = mode(img.to(device)).cpu()
#torch.squeeze移除张量中所有大小为1的维度,这里即移除Batchsize的维度
output = torch.squeeze(output)
#torch.softmax是将原始得分转为概率分布的函数,
#归一化:sotfmax将一个向量的元素映射为(0,1)区间,所有元素的和为1,形成概率分布
#对于多分类问题,softmax用于将模型的原始输出转为概率分布,表示每个类别的预测概率
#eg:输入是tensor([ 3.6085, -0.7472, -0.0099, -2.4376, -0.3151]),经过softmax输出是tensor([0.9418, 0.0121, 0.0253, 0.0022, 0.0186])
predict = torch.softmax(output,dim=0)
#找到概率最高的类别索引 eg:输入tensor([0.9418, 0.0121, 0.0253, 0.0022, 0.0186]) 输出:predic_cla:0
predict_cla= torch.argmax(predict).numpy()
print(f"predict:{predict},predic_cla:{predict_cla}")
Softmax可以通过以下数学公式实现:
四、flower_photos分类
使用Vgg16预训练模型的迁移学习,把全链接层1000类训练为5类的分类任务
数据集地址: https://storage.googleapis.com/download.tensorflow.org/example_images/flower_photos.tgz
训练集和验证集的划分
import os
from shutil import copy, rmtree
import random
def mk_file(file_path: str):
if os.path.exists(file_path):
# 如果文件夹存在,则先删除原文件夹在重新创建
rmtree(file_path)
os.makedirs(file_path)
def main():
# 保证随机可复现
random.seed(0)
# 将数据集中10%的数据划分到验证集中
split_rate = 0.1
# 指向你解压后的flower_photos文件夹
cwd = os.getcwd()
data_root = os.path.join(cwd, "flower_data")
origin_flower_path = os.path.join(data_root, "flower_photos")
assert os.path.exists(origin_flower_path), "path '{}' does not exist.".format(origin_flower_path)
flower_class = [cla for cla in os.listdir(origin_flower_path)
if os.path.isdir(os.path.join(origin_flower_path, cla))]
# 建立保存训练集的文件夹
train_root = os.path.join(data_root, "train")
mk_file(train_root)
for cla in flower_class:
# 建立每个类别对应的文件夹
mk_file(os.path.join(train_root, cla))
# 建立保存验证集的文件夹
val_root = os.path.join(data_root, "val")
mk_file(val_root)
for cla in flower_class:
# 建立每个类别对应的文件夹
mk_file(os.path.join(val_root, cla))
for cla in flower_class:
cla_path = os.path.join(origin_flower_path, cla)
images = os.listdir(cla_path)
num = len(images)
# 随机采样验证集的索引
eval_index = random.sample(images, k=int(num*split_rate))
for index, image in enumerate(images):
if image in eval_index:
# 将分配至验证集中的文件复制到相应目录
image_path = os.path.join(cla_path, image)
new_path = os.path.join(val_root, cla)
copy(image_path, new_path)
else:
# 将分配至训练集中的文件复制到相应目录
image_path = os.path.join(cla_path, image)
new_path = os.path.join(train_root, cla)
copy(image_path, new_path)
print("\r[{}] processing [{}/{}]".format(cla, index+1, num), end="") # processing bar
print()
print("processing done!")
#https://github.com/WZMIAOMIAO/deep-learning-for-image-processing/blob/master/data_set/split_data.py
调用上面的tran和predict 训练和推理,即可
五、资料
1.论文链接:https://arxiv.org/pdf/1409.1556
2.代码实现:https://github.com/WZMIAOMIAO/deep-learning-for-image-processing
3.图像分类篇——使用pytorch搭建VGG网络 https://blog.csdn.net/weixin_43872060/article/details/116607840
霹雳吧啦Wz https://space.bilibili.com/18161609/channel/series
4.VGG网络详解及感受野的计算 https://www.bilibili.com/video/BV1q7411T7Y6/?spm_id_from=333.999.0.0&vd_source=179014f1a2f3078fc78ff0659a14acb9
5.使用pytorch搭建VGG网络 https://www.bilibili.com/video/BV1i7411T7ZN/?spm_id_from=333.999.0.0&vd_source=179014f1a2f3078fc78ff0659a14acb9
6.VGG模型解析 https://github.com/huaweicloud/ModelArts-Lab/wiki/VGG%E6%A8%A1%E5%9E%8B%E8%A7%A3%E6%9E%90
7.VGG16网络结构详解 https://blog.csdn.net/light169/article/details/123270587
8.AlexNet网络结构详解https://blog.csdn.net/guzhao9901/article/details/118552085