模型容器与AlexNet构建
1. 网络层容器(Containers)
上一节我们学习了如何构建一个LeNet,回顾代码如下:
1 | import torch.nn as nn |
在__init__(self, classes)
中实现了网络层的初始化,在forward(self, x)
中完成了前向传播。
如果我们可以给网络层再进行分类,把网络层放到不同的容器里,那么可以方便我们构建网络。
pytorch提供了三种容器
1.1 容器之Sequential
nn.Sequential 是 nn.module的容器,用于按顺序包装一组网络层。这里我们以LeNet为例,前四层封装为features层,后3层封装为classifier层。
代码如下:
1 | import torch |
实验:
-
打断点,进入函数,观察net的初始化。
-
跳转到
class LeNetSequential(nn.Module)
的__init__(self, classes)
函数。单步运行nn.MaxPool2d
,进入再跳出(这是先完成nn.MaxPool2d
的实例化,我们不看这个),然后再次进入。 -
跳转到container.py的
class Sequential(Module)
的__init__(self, *args)
函数。可以看到,Sequential
也是nn.Module
的子类,所以他依然有8个有序字典属性。这里有一个判断语句。因为不是有序字典,所以会跳到else中。在else中会把传入的args依次检索出来,并用self.add_module
将网络层加入_module属性。 -
可以看一下传入的参数args,是一个长度为6的元组,就是我们在
class LeNetSequential(nn.Module)
的__init__(self, classes)
函数中定义的self.features
的6个网络层。 -
单步运行,我们可以发现Sequential的
_module
属性中已经添加了第一个网络层。依次运行,最后退出函数,跳转回到class LeNetSequential(nn.Module)
的__init__(self, classes)
函数。 -
这时候我们发现LeNetSequential的
_module
属性还是空的,这是因为刚刚完成的是self.features = nn.Sequential(...)
等号右边的初始化。再点击单步运行,发现LeNetSequential的_module
属性中多了一个元素,就是我们刚刚创建的feature,它是一个Sequential,里面有6个元素。我们查看feature的类属性的_module
属性,可以看到这6个网络层。 -
对于self.classifier的构建过程同理,不再赘述。
-
然后运行
fake_img = torch.randn((4, 3, 32, 32), dtype=torch.float32)
生成了一张假图片作为输入,再运行至output = net(fake_img)
,进入,观察前向传播的过程。 -
跳转到module.py的
class Module
的__call__
函数。只要是属于nn.module
类的,前向传播都会先进入这个__call__
函数。然后运行到result = self.forward(*input, **kwargs)
,进入。 -
跳转到主函数的
class LeNetSequential(nn.Module)
的forward(self, x)
函数,在这里完成前向传播(宏观的)。 -
进入
x = self.features(x)
,跳出(第一次进入是一个判断类属性赋值类型的函数,不关心)再进入就能到达module.py的class Module
的__call__
函数(因为Sequential也是属于nn.module
类)。然后运行到result = self.forward(*input, **kwargs)
,进入。 -
跳转到container.py的
class Sequential(Module)
的forward(self, input)
函数,在这里完成Sequential的网络层的前向传播(微观)。可以看到,这里用了一个for循环来依次取出Sequential内的网络层,然后依次将输入通过这些网络层。6次循环以后跳出函数,回到module.py的class Module
的__call__
函数。再运行会退出到LeNetSequential(nn.Module)
的forward(self, x)
函数, -
然后我们就完成了
self.features(x)
的前向传播。self.classifier(x)
的前向传播同理不赘述。最终我们就得到了输出x,返回x,退回到module.py的class Module
的__call__
函数。再次返回result退回到主函数。 -
于是,我们完成了一次前向传播过程,得到了output。
-
我们还可以运行
print(net)
,打印出我们模型的各层信息:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17LeNetSequential(
(features): Sequential(
(0): Conv2d(3, 6, kernel_size=(5, 5), stride=(1, 1))
(1): ReLU()
(2): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(3): Conv2d(6, 16, kernel_size=(5, 5), stride=(1, 1))
(4): ReLU()
(5): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
)
(classifier): Sequential(
(0): Linear(in_features=400, out_features=120, bias=True)
(1): ReLU()
(2): Linear(in_features=120, out_features=84, bias=True)
(3): ReLU()
(4): Linear(in_features=84, out_features=2, bias=True)
)
) -
可以发现,在Sequential内部的网络层还是用序号索引的(因为最外层的名字已经给features和classifier用掉了)。在上一节中,如果我们打印net信息,是可以用名称索引的(如下所示)。对于大型网络,用序号索引可能不方便,所以我们还有一种Sequential的方法。
1
2
3
4
5
6
7LeNet(
(conv1): Conv2d(3, 6, kernel_size=(5, 5), stride=(1, 1))
(conv2): Conv2d(6, 16, kernel_size=(5, 5), stride=(1, 1))
(fc1): Linear(in_features=400, out_features=120, bias=True)
(fc2): Linear(in_features=120, out_features=84, bias=True)
(fc3): Linear(in_features=84, out_features=2, bias=True)
) -
解决的方法很简单,之前我们在Sequential中传入的是元组。如果我们传入一个有序字典OrderedDict就能解决这个问题。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40import torch
import torchvision
import torch.nn as nn
from collections import OrderedDict
class LeNetSequential(nn.Module):
def __init__(self, classes):
super(LeNetSequentialOrderDict, self).__init__()
self.features = nn.Sequential(OrderedDict({
'conv1': nn.Conv2d(3, 6, 5),
'relu1': nn.ReLU(inplace=True),
'pool1': nn.MaxPool2d(kernel_size=2, stride=2),
'conv2': nn.Conv2d(6, 16, 5),
'relu2': nn.ReLU(inplace=True),
'pool2': nn.MaxPool2d(kernel_size=2, stride=2),
}))
self.classifier = nn.Sequential(OrderedDict({
'fc1': nn.Linear(16*5*5, 120),
'relu3': nn.ReLU(),
'fc2': nn.Linear(120, 84),
'relu4': nn.ReLU(inplace=True),
'fc3': nn.Linear(84, classes),
}))
def forward(self, x):
x = self.features(x)
x = x.view(x.size()[0], -1)
x = self.classifier(x)
return x
net = LeNetSequential(classes=2)
fake_img = torch.randn((4, 3, 32, 32), dtype=torch.float32)
output = net(fake_img)
print(net)
print(output) -
我们用同样的方法调试代码,这里就不赘述了。在进入Sequential的构建,即进入container.py的
class Sequential(Module)
的__init__(self, *args)
函数,又面临了和上面第三步一样的条件判断。这次我们当然进入的是if而不是else。对比if和else里面的语句,很容易就能发现刚刚为什么传入元组网络层的索引是序号,而传入有序字典网络层的索引就是名字了。 -
同样通过
print(net)
可以打印网络的结构,现在所有的网络层都是名字索引了。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17LeNetSequentialOrderDict(
(features): Sequential(
(conv1): Conv2d(3, 6, kernel_size=(5, 5), stride=(1, 1))
(relu1): ReLU(inplace=True)
(pool1): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(conv2): Conv2d(6, 16, kernel_size=(5, 5), stride=(1, 1))
(relu2): ReLU(inplace=True)
(pool2): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
)
(classifier): Sequential(
(fc1): Linear(in_features=400, out_features=120, bias=True)
(relu3): ReLU()
(fc2): Linear(in_features=120, out_features=84, bias=True)
(relu4): ReLU(inplace=True)
(fc3): Linear(in_features=84, out_features=2, bias=True)
)
)
总结:
nn.Sequential
是 nn.module
的容器,用于按顺序包装一组网络层(他的实现过程就是嵌套,比直接定义网络层多了一层封装。nn.Sequential
也是 nn.module
的子类,所以init
和forward
时和普通情况是一样的,都是调用同样的nn.module
的函数。先是构建Sequential
,然后是Sequential
内的网络层,相当于调用两次的nn.module
的函数。)
特点:
- 顺序性:各网络层之间严格按照顺序构建
- 自带forward():自带的forward里,通过for循环依次执行前向传播运算
1.2 容器之ModuleList
nn.ModuleList是 nn.module的容器,用于包装一组网络层,以迭代方式调用网络层
主要方法:
- append():在ModuleList后面添加网络层
- extend():拼接两个ModuleList
- insert():指定在ModuleList中位置插入网络层
代码如下:
1 | import torch |
实验:
-
打断点,进入
-
跳转到
class ModuleList(nn.Module)
的__init__(self)
函数。这里用了列表解析式生成20层10神经元的全连接层。 -
进入会跳转到container.py的
class ModuleList(Module)
的__init__(self, modules=None)
函数,可以发现这就是列表的拼接方法。所以ModuleList就是将网络层像列表一样拼接。 -
完成模型初始化跳出到主函数,可以看到net有linears属性,linears是一个ModuleList类。同样ModuleList类也是继承于nn.Module类的,所以linears也有8个有序字典属性。
-
运行
print(net)
可以打印出模型结构。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24oduleList(
(linears): ModuleList(
(0): Linear(in_features=10, out_features=10, bias=True)
(1): Linear(in_features=10, out_features=10, bias=True)
(2): Linear(in_features=10, out_features=10, bias=True)
(3): Linear(in_features=10, out_features=10, bias=True)
(4): Linear(in_features=10, out_features=10, bias=True)
(5): Linear(in_features=10, out_features=10, bias=True)
(6): Linear(in_features=10, out_features=10, bias=True)
(7): Linear(in_features=10, out_features=10, bias=True)
(8): Linear(in_features=10, out_features=10, bias=True)
(9): Linear(in_features=10, out_features=10, bias=True)
(10): Linear(in_features=10, out_features=10, bias=True)
(11): Linear(in_features=10, out_features=10, bias=True)
(12): Linear(in_features=10, out_features=10, bias=True)
(13): Linear(in_features=10, out_features=10, bias=True)
(14): Linear(in_features=10, out_features=10, bias=True)
(15): Linear(in_features=10, out_features=10, bias=True)
(16): Linear(in_features=10, out_features=10, bias=True)
(17): Linear(in_features=10, out_features=10, bias=True)
(18): Linear(in_features=10, out_features=10, bias=True)
(19): Linear(in_features=10, out_features=10, bias=True)
)
) -
运行到
output = net(fake_data)
,进入,观察前向传播过程。方法和之前一样,不再赘述。前向传播是在class ModuleList(nn.Module)
的forward
中实现的。
可见,使用ModuleList可以很方便的实现重复的网络结构。
1.3 容器之ModuleLDict
nn.ModuleDict是 nn.module的容器,用于包装一组网络层,以索引方式调用网络层
主要方法:
- clear():清空ModuleDict
- items():返回可迭代的键值对(key-value pairs)
- keys():返回字典的键(key)
- values():返回字典的值(value)
- pop():返回一对键值,并从字典中删除
代码如下:
1 | import torch |
- 在
self.choices = nn.ModuleDict
中定义了卷积层和池化层,在self.activations = nn.ModuleDict
中定义了两种激活函数。其实就是定义了两个字典,一个是self.choices
,一个是self.activations
,通过键(如’conv’)就可以访问对应的值(如nn.Conv2d(10, 10, 3)
)。 - output = net(fake_img, ‘conv’, ‘relu’)就是使用卷积层加relu激活函数。
实验:
-
打断点进入。
-
跳转到
class ModuleDict(nn.Module)
的__init__
,运行到最后进入container.py的class ModuleDict(Module)
的__init__(self, modules=None)
-
可以看到,这和ModuleList的结构非常相似,这里用了
self.update
函数向字典内添加元素。这就是ModuleList的初始化方法。 -
再看看它的前向传播实现。调试方法不再赘述,最终跳转到
class ModuleDict(nn.Module)
的forward
函数,前向传播就是在这里实现的。 -
进入
self.choices[choice](x)
,跳转到container.py的class ModuleDict(Module)
的__getitem__(self, key)
,在这里实现从ModuleDict
的_modules
属性中索引出我们需要的网络层。运行完会跳回class ModuleDict(nn.Module)
的forward
函数,继续完成前向传播。 -
最终得到了输出。
1.4 容器总结
- nn.Sequential:顺序性,各网络层之间严格按顺序执行,常用于block构建
- nn.ModuleList:迭代性,常用于大量重复网构建,通过for循环实现重复构建
- nn.ModuleDict:索引性,常用于可选择的网络层
2. AlexNet构建
学完了模型的构建以及容器,我们观察pytorch的torchvision模块中提供的AlexNet的构建。
实例化AlexNet的代码:
1 | alexnet = torchvision.models.AlexNet() |
进入alexnet.py中,代码如下:
1 | class AlexNet(nn.Module): |
可以看到,它使用了sequential的方法构建网络。
此外torchvision.models中还预设了许多其他网络,可以自行查看。