【研究生学习】Pytorch的官方代码搭建VGG模型
本篇博客是【深度学习个人笔记】Pytorch基本知识博客的扩展,采用里面介绍的Pytorch中的常用模块来搭建经典的VGG网络,使用的是Pytorch的官方代码,通过官方代码来体会如何用Pytorch搭建神经网络,并且学习如何保存神经网络的参数,并在GPU上进行训练
部分参考资料如下:
【Pytorch学习】-- 使用pytorch自带模型 -- VGG16
PyTorch中模型的parameters()方法浅析
使用pytorch搭建VGG网络 学习笔记
VGG网络的Pytorch官方实现过程解读
Pytorch(六)(模型参数的遍历) —— model.parameters() & model.named_parameters() & model.state_dict()
pytorch_神经网络模型搭建系列(1):自定义神经网络模型
PyTorch中的model.modules(), model.children(), model.named_children(), model.parameters(), model.nam...
深入浅出Pytorch函数——torch.nn.init.normal_
VGG基本介绍
VGG是非常经典的神经网络模型,其中比较常用的结构是VGG-16和VGG-19,16和19代表神经网络的层数,其具体网络结构如下表所示:
网络结构 | VGG-16 | VGG-19 |
---|---|---|
模块1 | conv3-64 conv3-64 |
conv3-64 conv3-64 |
下采样 | maxpool | maxpool |
模块2 | conv3-128 conv3-128 |
conv3-128 conv3-128 |
下采样 | maxpool | maxpool |
模块3 | conv3-256 conv3-256 conv3-256 |
conv3-256 conv3-256 conv3-256 conv3-256 |
下采样 | maxpool | maxpool |
模块4 | conv3-512 conv3-512 conv3-512 |
conv3-512 conv3-512 conv3-512 conv3-512 |
下采样 | maxpool | maxpool |
模块5 | conv3-512 conv3-512 conv3-512 |
conv3-512 conv3-512 conv3-512 conv3-512 |
下采样 | maxpool | maxpool |
全连接层 | FC-4096 | FC-4096 |
全连接层 | FC-4096 | FC-4096 |
全连接层 | FC-1000 | FC-1000 |
softmax分类输出层 | softmax | softmax |
上表中卷积层的参数表示为conv<卷积核大小>-<卷积核个数>,采用ReLU激活函数
可见VGG-16中包含了16个隐藏层(13个卷积层和3个全连接层),VGG-19中包含了19个隐藏层(16个卷积层和3个全连接层)
可以用可视化的形式来理解VGG的网络结构,以VGG-16为例:
上图中黑色边框代表卷积层和ReLU激活函数。红色边框代表最大池化,黄色边框代表softmax,蓝色边框代表全连接层和ReLU激活函数
Pytorch搭建VGG-16和VGG-19
在Pytorch中调用实现好的VGG-16和VGG-19
Pytorch已经实现了很多经典模型,VGG就是其中之一,同时现在我们使用VGG网络通常是采用预训练模型。在Pytorch中直接调用VGG可以用torchvision.models,并可以自己选择是否使用预训练的模型:
import torchvision
vgg16 = torchvision.models.vgg16(pretrained = False)
vgg16_pretrained = torchvision.models.vgg16(pretrained = True)
vgg19 = torchvision.models.vgg19(pretrained = False)
vgg19_pretrained = torchvision.models.vgg19(pretrained = True)
可以直接通过print查看网络结构和网络参数(将下面代码中的net替换为上面的模型变量即可):
print(net)
print(net.parameters())
print(net.named_parameters())
对于直接打印模型变量,输出结果如下(以上面的vgg16为例):
VGG(
(features): Sequential(
(0): Conv2d(3, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(1): ReLU(inplace=True)
(2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(3): ReLU(inplace=True)
(4): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(5): Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(6): ReLU(inplace=True)
(7): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(8): ReLU(inplace=True)
(9): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(10): Conv2d(128, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(11): ReLU(inplace=True)
(12): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(13): ReLU(inplace=True)
(14): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(15): ReLU(inplace=True)
(16): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(17): Conv2d(256, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(18): ReLU(inplace=True)
(19): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(20): ReLU(inplace=True)
(21): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(22): ReLU(inplace=True)
(23): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(24): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(25): ReLU(inplace=True)
(26): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(27): ReLU(inplace=True)
(28): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(29): ReLU(inplace=True)
(30): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
)
(avgpool): AdaptiveAvgPool2d(output_size=(7, 7))
(classifier): Sequential(
(0): Linear(in_features=25088, out_features=4096, bias=True)
(1): ReLU(inplace=True)
(2): Dropout(p=0.5, inplace=False)
(3): Linear(in_features=4096, out_features=4096, bias=True)
(4): ReLU(inplace=True)
(5): Dropout(p=0.5, inplace=False)
(6): Linear(in_features=4096, out_features=1000, bias=True)
)
)
可见与之前表中的VGG-16的网络结构一致
而对于直接打印模型变量,输出结果如下(以上面的vgg16为例):
<generator object Module.named_parameters at 0x00000183F6A9EF90>
<generator object Module.parameters at 0x00000183F6A9EF90>
可见.parameters()方法和.named_parameters()方法会返回一个生成器(迭代器),可以直接将其转换为列表并打印出来(这样子做的结果就是一堆tensor),也可以用enumerate,只不过对于.named_parameters()方法而言返回的是len为2的tuple,第一个元素是name,第二个元素是name对应的值,如下所示:
for _,param in enumerate(net.named_parameters()):
print(param[0])
print(param[1])
print('----------------')
for _,param in enumerate(net.parameters()):
print(param)
print('----------------')
Pytorch官方实现VGG的过程
如何参考Pytorch官方实现VGG的过程
在上一小节中,调用VGG模型使用了torchvision.models,实际上说的更具体些,是models里的vgg.py,具体在PC上的路径如下(其中的torch是自己在anaconda创建的环境名,需要根据实际情况修改):
Anaconda3\envs\torch\lib\site-packages\torchvision\models\vgg.py
下面就看一下Pytorch官方实现VGG-16和VGG-19的过程,学习一下Pytorch的用法,顺便学习一些里面用到的Python知识,值得一提的是,这里使用的是Python版本是3.9,Pytorch版本是1.12,CUDA版本是11.6,可以通过如下的代码获取:
print("Python version:", sys.version)
print("Pytorch version:", torch.__version__)
print("CUDA version:", torch.version.cuda)
在我的电脑上的运行结果如下:
Python version: 3.9.13 (main, Oct 13 2022, 21:23:06) [MSC v.1916 64 bit (AMD64)]
Pytorch version: 1.12.0
CUDA version: 11.6
vgg.py
- 在vgg.py中首先定义了一个列表,很显然列表里的内容对应着Pytorch实现的与VGG相关的不同模型种类,具体这些模型有什么区别可以等后面看到具体的类或函数再来深究(这里只关注和VGG-16和VGG-19相关的模型):
__all__ = [
"VGG",
"VGG11_Weights",
"VGG11_BN_Weights",
"VGG13_Weights",
"VGG13_BN_Weights",
"VGG16_Weights",
"VGG16_BN_Weights",
"VGG19_Weights",
"VGG19_BN_Weights",
"vgg11",
"vgg11_bn",
"vgg13",
"vgg13_bn",
"vgg16",
"vgg16_bn",
"vgg19",
"vgg19_bn",
]
- 接下来在vgg.py中定义了VGG通用网络结构,这是非常标准的定义自己的神经网络模型的方法,通过继承nn.Module类并写好forward方法:
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),
)
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: torch.Tensor) -> torch.Tensor:
x = self.features(x)
x = self.avgpool(x)
x = torch.flatten(x, 1)
x = self.classifier(x)
return x
可见Pytorch官方在forward方法中将VGG通用网络结构分为了三个部分:features、avgpool和classifier,其实这对应的是上面的VGG网络结构表中的模块5的后面的部分,即通过前面的卷积神经网络(不同的VGG类型,比如VGG-16和VGG-19对应的卷积神经网络结构不同)提取特征,通过自适应平均池化(nn.AdaptiveAvgPool2d)将特征图池化到固定尺寸7×7大小,然后将特征图展平为1维向量(torch.flatten(x, 1)),最后通过分类器,即三层全连接层输出对应类别数量维度的向量(这里默认是1000维,即在类的初始化中输入的num_classes: int = 1000)
上面的代码除了定义VGG通用网络结构外,还有关于权重初始化的部分,即在类的初始化方法中if init_weights:后面的内容,这里的判断条件init_weights默认为True(init_weights: bool = True),而self.modules()会返回一个迭代器,会迭代遍历模型的所有子层(即nn.Module的子类,这里VGG、features、avgpool、classifier、池化、Linear等都是nn.Module的子类);isinstance()用于判断一个对象是否是一个已知的类型,同时会认为子类是一种父类类型,考虑继承关系。
根据上面的分析,这里的if判断语句分为了三个部分:nn.Conv2d、nn.BatchNorm2d和nn.Linear,分别对这三部分进行初始化,初始化函数有三种,其功能分别如下:
(1)nn.init.kaiming_normal_:何恺明大神提出的参数初始化(论文:Delving deep into rectifiers: Surpassing human-level performance on ImageNet classification)
(2)nn.init.constant_(tensor, val):用val的值填充整个张量
(3)nn.init.normal_(tensor, mean=0.0, std=1.0):从给定均值和标准差的正态分布中生成值,填充输入的张量
介绍了Pytorch搭建神经网络的相关知识,下面就介绍一下代码中有关Python的技巧:
(1)在初始化方法和forward方法传入的参数中,每一个都有一个冒号,比如num_classes: int = 1000,这些参数的冒号其实是参数的类型建议符,目的是告诉使用程序的人希望传入的实参的类型,类型建议符并非强制规定和检查,即使传入的实际参数与建议参数不符,也不会报错
(2)同时初始化方法和forward方法后面都跟着一个箭头,比如-> torch.Tensor,这是说明该方法返回的值是什么类型
- 接下来在vgg.py中定义了函数make_layers和_vgg以及字典cfgs和_COMMON_META,这些都是构成后面具体网络结构,比如VGG-16和VGG-19的基础:
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:
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)
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"],
}
def _vgg(cfg: str, batch_norm: bool, weights: Optional[WeightsEnum], progress: bool, **kwargs: Any) -> 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], batch_norm=batch_norm), **kwargs)
if weights is not None:
model.load_state_dict(weights.get_state_dict(progress=progress))
return model
_COMMON_META = {
"min_size": (32, 32),
"categories": _IMAGENET_CATEGORIES,
"recipe": "https://github.com/pytorch/vision/tree/main/references/classification#alexnet-and-vgg",
"_docs": """These weights were trained from scratch by using a simplified training recipe.""",
}
可见make_layers函数是根据配置表返回模型层列表;而_vgg函数实现了整个网络模型的生成以及预训练权重的导入。由于上面的代码涉及到一些新的数据类型,这里先对这些进行讲解,即List、Union、Dict,这些数据类型来自typing模块
最后也介绍一下这一段代码使用的Python技巧:
- 在make_layers函数的返回值中,其返回中传入的参数带有*,即nn.Sequential(*layers),事实上,很多传入的参数还带有两个星号