背景
数据处理做好了,现在搭建网络
声明:整个数据和代码来自于b站,链接:使用pytorch框架手把手教你利用VGG16网络编写猫狗分类程序_哔哩哔哩_bilibili
我做了复现,并且记录了自己在做这个项目分类时候,一些所思所得。
VGG16,就是C这样,叫16是因为,卷积+全连接层,一共有6层,因为这俩有可以学习的w,池化层是没有的,所以叫做,VGG16
VGG 16的样子
VGG16网络pytorch搭建
pytorch官网有很多网络的源码:下面是VGG的源码
https://github.com/pytorch/vision/blob/main/torchvision/models/vgg.py
然后,针对于源码,进行改动就行了,因为我们要用VGG16
把源码复制到“return model”就行了,接下来,自己删删改改。
所以,学会用pytorch官网真的重要,多看看,多摸索。
VGG16网络代码拆解
现在,对于VGG16的pytorch官网的代码,每一块进行拆解,理解为什么是这样搭建的,以及,以后自己搭建,要怎么搭建,sop是什么。
1、导包
from functools import partial
from typing import Any, cast, Dict, List, Optional, Union
import torch
import torch.nn as nn
from ..transforms._presets import ImageClassification
from ..utils import _log_api_usage_once
from ._api import register_model, Weights, WeightsEnum
from ._meta import _IMAGENET_CATEGORIES
from ._utils import _ovewrite_named_param, handle_legacy_interface
- 官网代码如上,但是很多都不需要,最后保留如下:
import torch
import torch.nn as nn
from torch.hub import load_state_dict_from_url
最后一行:导入PyTorch中的load_state_dict_from_url函数。这个函数是用来从指定的URL加载模型的状态字典(state dict)。状态字典是一个Python字典对象,它存储了模型中所有可学习参数(如权重和偏置)的值。这对于加载预训练模型的权重非常有用,因为你可以直接将这些权重载入到你定义的相同结构的模型中,从而复用预训练模型学到的特征。
使用这个函数的一般步骤包括:
- 定义模型结构。
- 通过URL指定预训练模型权重的位置。URL可以指向的不仅仅是网页,它还可以指向图片、视频、文档、API端点,甚至是托管在云存储服务上的模型文件等任何类型的网络资源。在这里,具体来说,它是指向预训练模型权重文件的直接链接。这个文件通常是以.pth或.pkl等格式存储的PyTorch模型状态字典文件。通过这个URL,PyTorch可以从远程服务器下载这些预训练权重,并将其加载到你的模型中。
- 调用load_state_dict_from_url
(url)
下载并加载权重。 - 将下载的权重状态字典赋给模型实例的
.state_dict()
,以恢复模型的参数。
这样就可以快速地利用已有的预训练模型进行迁移学习或者作为新任务的起点,而无需从头开始训练模型。
删除_all_列表:官网把所有VGG网络列在列表里,不需要,直接删除
- 下载预训练权重
model_urls = { "vgg16": "https://download.pytorch.org/models/vgg16-397923af.pth", }#权重下载网址
2、搭建VGG16网络
2.1 定义init函数,构建VGG16模型中处理分类决策的部分
搭架子,搭建网络架子,就是先把这个VGG16网络,里面的算子层,按照VGG16那个结构,搭建起来,从开始知道,它一共是16层,按照顺序,搭建起来。但是这一块和自己搭建不一样,他不是先一层一层搭建的,它先把分类器给搭建了。
把官方源代码复制过来,删除一些不必要的,用删除线表示,新增用斜线表示。
这里分为两块,网络搭建+权重初始化
先看看网络搭建部分,传特征,建池化层,按照顺序搭建
class VGG(nn.Module):
def __init__(self, features: nn.Module, num_classes: int = 1000, init_weights: bool = True, dropout: float = 0.5) -> None:
super().__init__()
_log_api_usage_once(self)
self.features = features # 传特征
self.avgpool = nn.AdaptiveAvgPool2d((7, 7))
self.classifier = nn.Sequential(
nn.Linear(512 * 7 * 7, 4096),
nn.ReLU(True),
nn.Dropout(p=dropout),
nn.Linear(4096, 4096),
nn.ReLU(True),
nn.Dropout(p=dropout),
nn.Linear(4096, num_classes),
)
注意点:
- init_weights
: bool= True 这里意思是使用初始权重 - self.classifier = nn.Sequential(....)这段代码实际上展示的是VGG网络后面的部分,即全连接层(FC)部分,通常紧随在卷积和池化层之后,用于图像分类任务的最终分类。
2.2 模型创建时根据层的类型自动初始化权重和偏置
if init_weights:
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight, mode="fan_out", nonlinearity="relu")
if m.bias is not None:
nn.init.constant_(m.bias, 0)
elif isinstance(m, nn.BatchNorm2d):
nn.init.constant_(m.weight, 1)
nn.init.constant_(m.bias, 0)
elif isinstance(m, nn.Linear):
nn.init.normal_(m.weight, 0, 0.01)
nn.init.constant_(m.bias, 0)
这段代码是用于神经网络模型中权重初始化的部分,它遍历模型的所有模块并根据模块的类型应用特定的初始化方法。逐条解释:
1. 条件判断:
if init_weights: 这行代码检查是否需要初始化权重。如果`init_weights`为`True`,则执行下面的初始化逻辑。
2. 遍历所有模块:
for m in self.modules():
使用`self.modules()`方法遍历模型中的所有子模块,就是把所有的算子访问一遍。这包括所有直接或间接属于该模型的层(如卷积层、线性层、批归一化层等)。
3. 卷积层(nn.Conv2d)的初始化:
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight, mode="fan_out", nonlinearity="relu")
if m.bias is not None:
nn.init.constant_(m.bias, 0)
- 当检测到模块`m`是卷积层(`nn.Conv2d`)时,使用`kaiming_normal_`函数按照"he初始化"方法初始化权重。这种初始化方法特别适合ReLU激活函数,其中`mode="fan_out"`表示根据输出通道的数量调整权重的初始化范围,以保持输出的方差接近1,有利于训练的稳定性。
- 如果卷积层包含偏置项(`bias`),则将其初始化为0。
4. 批量归一化层(nn.BatchNorm2d)的初始化:
elif isinstance(m, nn.BatchNorm2d):
nn.init.constant_(m.weight, 1)
nn.init.constant_(m.bias, 0)
- 当模块是批量归一化层时,其缩放因子(`weight`)被初始化为1,偏移量(`bias`)被初始化为0。这确保了在训练初期,批量归一化层不会改变输入数据的分布。
5. 线性层(nn.Linear)的初始化:
elif isinstance(m, nn.Linear):
nn.init.normal_(m.weight, 0, 0.01)
nn.init.constant_(m.bias, 0)
- 对于线性层,权重使用正态分布初始化,均值为0,标准差为0.01。这样的初始化策略有助于促进网络的学习过程。
- 线性层的偏置同样被初始化为0。
总结起来,这段代码的作用是在模型创建时根据层的类型自动初始化权重和偏置,采用了针对不同层类型最优的初始化策略,有助于模型训练的快速收敛和良好的泛化能力。
2.2 进行前向传播
def forward(self, x) :
x = self.features(x)
x = self.avgpool(x)
x = torch.flatten(x, 1)
x = self.classifier(x)
return x
注意:
- x = self.features(x)这一行调用了模型中定义的特征提取部分(self.features),它通常由多个卷积层、激活函数和池化层组成。输入x经过这些层后,转换为更高层次的特征表示。
- x = torch.flatten(x, 1)将三维的特征图(形状可能是[N, C, H, W],其中N是批量大小,C是通道数,H和W是高度和宽度)展平为二维张量(形状变为[N, CHW]),以便能够输入到全连接层中。这里
1
表示沿第二个维度(通道维)开始展平。
2.3 定义按照cfg配置文件来进行顺序排列的算子层
def make_layers(cfg: List[Union[str, int]], batch_norm: bool = False) -> nn.Sequential:
layers: List[nn.Module] = []
in_channels = 3
for v in cfg: # make_layers对输入的cfg进行循环
if v == "M":
layers += [nn.MaxPool2d(kernel_size=2, stride=2)]
else:
v = cast(int, v)
conv2d = nn.Conv2d(in_channels, v, kernel_size=3, padding=1)
if batch_norm:
layers += [conv2d, nn.BatchNorm2d(v), nn.ReLU(inplace=True)]
else:
layers += [conv2d, nn.ReLU(inplace=True)]
in_channels = v
return nn.Sequential(*layers)
解析:
这段代码定义了一个名为`make_layers`的函数,用于根据给定的配置`cfg`创建一系列卷积层(以及可选的批量归一化层和激活函数),最终将这些层组合成一个`nn.Sequential`模块。
- 函数签名:def make_layers(cfg: List[Union[str, int]], batch_norm: bool = False) -> nn.Sequential:
- `cfg`: 一个列表,包含字符串"М"和整数。字符串"M"代表一个最大池化层,整数表示卷积层的输出通道数。
- `batch_norm`: 布尔值,默认为`False`,决定是否在每个卷积层后添加批量归一化层。
- 函数返回一个`nn.Sequential`对象,包含根据`cfg`构建的所有层。
- 变量初始化:
layers: List[nn.Module] = []
in_channels = 3
初始化一个空列表`layers`来存储创建的层,`in_channels`初始化为3,代表输入图像的通道数(通常为RGB图像)。
- 循环构建层:for v in cfg:遍历配置列表`cfg`中的每个元素`v`。
- 如果`v`等于字符串"M",则添加一个最大池化层:
layers += [nn.MaxPool2d(kernel_size=2, stride=2)]
这会减少特征图的空间尺寸,但保持通道数不变。
- 否则,`v`被视为整数,表示下一个卷积层的输出通道数。创建一个卷积层:
conv2d = nn.Conv2d(in_channels, v, kernel_size=3, padding=1)
这个卷积层使用3x3的卷积核,padding为1以保持输入输出尺寸相同,`in_channels`是输入通道数,`v`是输出通道数。
- 根据`batch_norm`的值,决定是否在卷积层后添加批量归一化和ReLU激活函数:
if batch_norm:
layers += [conv2d, nn.BatchNorm2d(v), nn.ReLU(inplace=True)]
else:
layers += [conv2d, nn.ReLU(inplace=True)]
如果开启批量归一化,会在卷积后紧跟`nn.BatchNorm2d`和激活函数`nn.ReLU`。`inplace=True`意味着ReLU操作直接修改输入数据,节省内存。
- 更新`in_channels`为当前卷积层的输出通道数,准备构建下一层。
- 返回Sequential模型:return nn.Sequential(*layers):将构建好的层列表`layers`作为参数传递给`nn.Sequential`,创建一个有序的神经网络模块,其中每一层依次执行。
这个函数非常实用,可以根据配置灵活生成具有或不具有批量归一化的卷积神经网络层序列,常用于构建经典的卷积神经网络架构。
2.4 创建cfg配置文件
cfgs: Dict[str, List[Union[str, int]]] = {
"A": [64, "M", 128, "M", 256, 256, "M", 512, 512, "M", 512, 512, "M"],
"B": [64, 64, "M", 128, 128, "M", 256, 256, "M", 512, 512, "M", 512, 512, "M"],
"D": [64, 64, "M", 128, 128, "M", 256, 256, 256, "M", 512, 512, 512, "M", 512, 512, 512, "M"],
"E": [64, 64, "M", 128, 128, "M", 256, 256, 256, 256, "M", 512, 512, 512, 512, "M", 512, 512, 512, 512, "M"],
}
在官方提供的配置字典cfgs中,VGG16对应的配置是"D
",是一个字典。类似于yaml文件。
选择cfgs["D"]
作为参数传递给make_layers
函数。
2.5 定义 _vgg函数,用于根据给定的配置创建VGG模型,并可选地加载预训练权重
官方代码:
def _vgg(cfg: str, batch_norm: bool, weights: Optional[WeightsEnum], progress: bool, **kwargs: Any
pretrained=False, progress=True,num_classes=2
) -> VGG:
if weights is not None:
kwargs["init_weights"] = False
if weights.meta["categories"] is not None:
_ovewrite_named_param(kwargs, "num_classes", len(weights.meta["categories"]))
model = VGG(make_layers(cfgs[cfg'D'], batch_norm=batch_norm), **kwargs)
if weights is not None: # 就是说要预训练
state_dict = load_state_dict_from_url(model_urls['vgg16'],model_dir='./model' ,progress=progress)#预训练模型地址
model.load_state_dict(state_dict)
model.load_state_dict(weights.get_state_dict(progress=progress, check_hash=True))
return model
- 在官方的代码中,有这样一个逻辑段:
if weights is not None:
kwargs["init_weights"] = False
if weights.meta["categories"] is not None:
_ovewrite_named_param(kwargs, "num_classes", len(weights.meta["categories"]))
这段代码的作用是,当提供了预训练权重(`weights`不为`None`)时,执行以下操作:
1. **关闭自定义权重初始化**:`kwargs["init_weights"] = False`,因为预训练模型已经有了权重,不需要再执行自定义的权重初始化。
2. **调整输出类别数**:如果预训练权重的元数据中包含类别信息(`weights.meta["categories"]`),则使用该预训练权重对应的类别数(`len(weights.meta["categories"])`)来覆盖模型的输出类别数。这是因为预训练模型通常是为了特定任务训练的,比如ImageNet数据集有1000类,所以预训练权重的输出层大小是固定的。如果新任务的类别数不同,需要调整模型的输出层以匹配新任务的类别数。
因此,原本的逻辑考虑了如果使用预训练权重,模型的输出层(通常是最后的全连接层)的大小需要根据预训练权重的类别数来调整。但是猫狗识别只需识别两类,而预训练权重是为更多类设计的(比如1000类),直接使用不调整会导致模型结构不匹配,所以需要这个逻辑来自动调整模型的输出层以匹配预训练权重或特定任务的需求。但因为明确知道输出类只有2个,且可能不打算使用预训练权重,所以可以省略这部分调整逻辑。
- 因为我们会使用预训练权重来初始化模型的一部分或全部权重,那么加载预训练权重的逻辑应当保留
if weights is not None: model.load_state_dict(weights.get_state_dict(progress=progress, check_hash=True))
这段代码确保了当提供了预训练权重(weights
不为None
)时,模型会加载这些预训练权重。weights.get_state_dict()
会获取预训练权重的状态字典,而model.load_state_dict(...)
则将这些权重加载到模型中。progress=True
表示在下载预训练权重文件时显示进度条,check_hash=True
则会检查下载的权重文件的哈希值,确保文件的完整性和一致性。如果你打算利用预训练权重进行迁移学习或微调,保留这行及其相关逻辑是必要的。
- 新增:分类器修改问题,因为原来的网络是针对于1000个类别构建的现在我们只有猫和狗两个类别,所以,通常的做法就是,保留特征提取部分,什么卷积之类的,在最后一层,分类器构建的时候,直接复制前面,它根据1000个输出类别构建的分类器网络,然后把最后一层的输出改成2类,然后记得,把前面传进去的类别也改成两类。
if num_classes !=1000:
model.classifier = nn.Sequential(
nn.Linear(512 * 7 * 7, 4096),
nn.ReLU(True), nn.Dropout(p=0.5),#随机删除一部分不合格
nn.Linear(4096, 4096),
nn.ReLU(True),#防止过拟合
nn.Dropout(p=0.5),
nn.Linear(4096, num_classes), )
-
为什么pretrain=false?不是要进行预训练吗
在这个函数定义中,`pretrained`是一个布尔参数,用于控制是否加载预训练权重。参数名设为`pretrained=False`是作为默认值,意味着如果不特别指定,函数将不加载预训练权重,而是创建一个未经训练的模型。这给予了使用者灵活性:他们可以选择使用预训练模型进行迁移学习(通过设置`pretrained=True`),或者从头开始训练一个模型(保留默认值`pretrained=False`)。
因此,`pretrained=False`并不代表不进行预训练,而是表示函数默认行为是不自动加载预训练权重。如果用户想要加载预训练权重,他们需要显式地将`pretrained`参数设置为`True`,如`vgg16(pretrained=True, ...)`。这样设计允许该函数既可用于初始化一个全新的模型用于从头训练,也可用于基于预训练模型的微调或特征提取任务。
2.6 return model
完整代码
import torch
import torch.nn as nn
from torch.hub import load_state_dict_from_url
model_urls = {
"vgg16": "https://download.pytorch.org/models/vgg16-397923af.pth",
}#权重下载网址
class VGG(nn.Module):
def __init__(self, features, num_classes = 1000, init_weights= True, dropout = 0.5):
super(VGG,self).__init__()
self.features = features
self.avgpool = nn.AdaptiveAvgPool2d((7, 7))#AdaptiveAvgPool2d使处于不同大小的图片也能进行分类
self.classifier = nn.Sequential(
nn.Linear(512 * 7 * 7, 4096),
nn.ReLU(True),
nn.Dropout(p=dropout),
nn.Linear(4096, 4096),
nn.ReLU(True),
nn.Dropout(p=dropout),#完成4096的全连接
nn.Linear(4096, num_classes),#对num_classes的分类
)
if init_weights:
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight, mode="fan_out", nonlinearity="relu")
if m.bias is not None:
nn.init.constant_(m.bias, 0)
elif isinstance(m, nn.BatchNorm2d):
nn.init.constant_(m.weight, 1)
nn.init.constant_(m.bias, 0)
elif isinstance(m, nn.Linear):
nn.init.normal_(m.weight, 0, 0.01)
nn.init.constant_(m.bias, 0)
def forward(self, x):
x = self.features(x)
x = self.avgpool(x)
x = torch.flatten(x, 1)#对输入层进行平铺,转化为一维数据
x = self.classifier(x)
return x
def make_layers(cfg, batch_norm = False):#make_layers对输入的cfg进行循环
layers = []
in_channels = 3
for v in cfg:#对cfg进行输入循环,取第一个v
if v == "M":
layers += [nn.MaxPool2d(kernel_size=2, stride=2)]#把输入图像进行缩小
else:
conv2d = nn.Conv2d(in_channels, v, kernel_size=3, padding=1)#输入通道是3,输出通道64
if batch_norm:
layers += [conv2d, nn.BatchNorm2d(v), nn.ReLU(inplace=True)]
else:
layers += [conv2d, nn.ReLU(inplace=True)]
in_channels = v
return nn.Sequential(*layers)
cfgs = {
"D": [64, 64, "M", 128, 128, "M", 256, 256, 256, "M", 512, 512, 512, "M", 512, 512, 512, "M"],
}
def vgg16(pretrained=False, progress=True,num_classes=2):
model = VGG(make_layers(cfgs['D']))
if pretrained:
state_dict = load_state_dict_from_url(model_urls['vgg16'],model_dir='./model' ,progress=progress)#预训练模型地址
model.load_state_dict(state_dict)
if num_classes !=1000:
model.classifier = nn.Sequential(
nn.Linear(512 * 7 * 7, 4096),
nn.ReLU(True),
nn.Dropout(p=0.5),#随机删除一部分不合格
nn.Linear(4096, 4096),
nn.ReLU(True),#防止过拟合
nn.Dropout(p=0.5),
nn.Linear(4096, num_classes),
)
return model
总结
- 构建网络步骤:
- 构建VGG网络,继承nn.module模块:构造init函数,搭建算子,主要是把分类器做好,权重初始化,然后设定做前向传播。
- 根据cfg做算子层的按照顺序排列,make_layers
- 列出来cfg配置文件
- 定义vgg16的model模型,调用make_layers和cfg配置文件匹配上:model = VGG(make_layers(cfgs['D']))
- 注意:模型是一整个的概念,网络只是模型中的一个环节。
通过使用配置`cfgs['D']`来创建一个VGG模型实例,并将其赋值给变量`model`。这里的`VGG`是一个之前定义好的类,它用于构建具有特定结构的卷积神经网络,该结构遵循VGG16的设计。而`make_layers(cfgs['D'])`是一个函数调用,它接收一个配置参数(在这个例子中是`cfgs['D']`),并根据这个配置生成一系列的卷积层和池化层,这些层构成了VGG模型的主体部分——特征提取层。
具体来说:
- `cfgs['D']`通常是一个列表,其中包含了描述每一层类型(比如卷积层或最大池化层)和该层输出通道数的元组。对于VGG16,这个配置定义了多个卷积层后跟着最大池化层的重复模式,直到达到网络的高层部分。
- `make_layers`函数遍历这个配置列表,根据每个条目创建相应的层(使用`nn.Conv2d`和`nn.MaxPool2d`等PyTorch模块),并将它们添加到一个有序的层列表中,最终返回这个列表。这个过程实质上是把VGG的架构从配置信息转换为实际可执行的神经网络层结构。
因此,`model = VGG(make_layers(cfgs['D']))`实质上是构建了一个完整的VGG16模型,准备用于后续的训练或推理任务。