模型容器与AlexNet构建

1. 网络层容器(Containers)

上一节我们学习了如何构建一个LeNet,回顾代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import torch.nn as nn
import torch.nn.functional as F

class LeNet(nn.Module):
def __init__(self, classes):
super(LeNet, self).__init__()
self.conv1 = nn.Conv2d(3, 6, 5)
self.conv2 = nn.Conv2d(6, 16, 5)
self.fc1 = nn.Linear(16*5*5, 120)
self.fc2 = nn.Linear(120, 84)
self.fc3 = nn.Linear(84, classes)

def forward(self, x):
out = F.relu(self.conv1(x))
out = F.max_pool2d(out, 2)
out = F.relu(self.conv2(out))
out = F.max_pool2d(out, 2)
out = out.view(out.size(0), -1)
out = F.relu(self.fc1(out))
out = F.relu(self.fc2(out))
out = self.fc3(out)
return out

__init__(self, classes)中实现了网络层的初始化,在forward(self, x)中完成了前向传播。

如果我们可以给网络层再进行分类,把网络层放到不同的容器里,那么可以方便我们构建网络。

pytorch提供了三种容器

image-20200728164642216

1.1 容器之Sequential

nn.Sequential 是 nn.module的容器,用于按顺序包装一组网络层。这里我们以LeNet为例,前四层封装为features层,后3层封装为classifier层。

image-20200728165214229

代码如下:

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
import torch
import torchvision
import torch.nn as nn
from collections import OrderedDict

class LeNetSequential(nn.Module):
def __init__(self, classes):
super(LeNetSequential, self).__init__()
self.features = nn.Sequential(
nn.Conv2d(3, 6, 5),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2, stride=2),
nn.Conv2d(6, 16, 5),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2, stride=2),)

self.classifier = nn.Sequential(
nn.Linear(16*5*5, 120),
nn.ReLU(),
nn.Linear(120, 84),
nn.ReLU(),
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)

实验:

  1. 打断点,进入函数,观察net的初始化。

    image-20200728171231583

  2. 跳转到class LeNetSequential(nn.Module)__init__(self, classes)函数。单步运行nn.MaxPool2d,进入再跳出(这是先完成nn.MaxPool2d的实例化,我们不看这个),然后再次进入。

    image-20200728172049298

  3. 跳转到container.py的class Sequential(Module)__init__(self, *args)函数。可以看到,Sequential也是nn.Module的子类,所以他依然有8个有序字典属性。这里有一个判断语句。因为不是有序字典,所以会跳到else中。在else中会把传入的args依次检索出来,并用self.add_module将网络层加入_module属性。

    image-20200728173437871

  4. 可以看一下传入的参数args,是一个长度为6的元组,就是我们在class LeNetSequential(nn.Module)__init__(self, classes)函数中定义的self.features的6个网络层。

    image-20200728173144955

  5. 单步运行,我们可以发现Sequential的_module属性中已经添加了第一个网络层。依次运行,最后退出函数,跳转回到class LeNetSequential(nn.Module)__init__(self, classes)函数。

    image-20200728173603896

  6. 这时候我们发现LeNetSequential的_module属性还是空的,这是因为刚刚完成的是self.features = nn.Sequential(...)等号右边的初始化。再点击单步运行,发现LeNetSequential的_module属性中多了一个元素,就是我们刚刚创建的feature,它是一个Sequential,里面有6个元素。我们查看feature的类属性的_module属性,可以看到这6个网络层。

    image-20200728173956038

    image-20200728174240123

  7. 对于self.classifier的构建过程同理,不再赘述。

    image-20200728175154159

    image-20200728175208423

  8. 然后运行fake_img = torch.randn((4, 3, 32, 32), dtype=torch.float32)生成了一张假图片作为输入,再运行至output = net(fake_img),进入,观察前向传播的过程。

    image-20200728175532119

  9. 跳转到module.py的class Module__call__函数。只要是属于nn.module类的,前向传播都会先进入这个__call__函数。然后运行到result = self.forward(*input, **kwargs),进入。

    image-20200728175710118

  10. 跳转到主函数的class LeNetSequential(nn.Module)forward(self, x)函数,在这里完成前向传播(宏观的)。

    image-20200728180613910

  11. 进入x = self.features(x),跳出(第一次进入是一个判断类属性赋值类型的函数,不关心)再进入就能到达module.py的class Module__call__函数(因为Sequential也是属于nn.module类)。然后运行到result = self.forward(*input, **kwargs),进入。

    image-20200728175710118

  12. 跳转到container.py的class Sequential(Module)forward(self, input)函数,在这里完成Sequential的网络层的前向传播(微观)。可以看到,这里用了一个for循环来依次取出Sequential内的网络层,然后依次将输入通过这些网络层。6次循环以后跳出函数,回到module.py的class Module__call__函数。再运行会退出到LeNetSequential(nn.Module)forward(self, x)函数,

    image-20200728181701303

  13. 然后我们就完成了self.features(x)的前向传播。self.classifier(x)的前向传播同理不赘述。最终我们就得到了输出x,返回x,退回到module.py的class Module__call__函数。再次返回result退回到主函数。

    image-20200728182017512

  14. 于是,我们完成了一次前向传播过程,得到了output。

    image-20200728182310733

  15. 我们还可以运行print(net),打印出我们模型的各层信息:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    LeNetSequential(
    (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)
    )
    )
  16. 可以发现,在Sequential内部的网络层还是用序号索引的(因为最外层的名字已经给features和classifier用掉了)。在上一节中,如果我们打印net信息,是可以用名称索引的(如下所示)。对于大型网络,用序号索引可能不方便,所以我们还有一种Sequential的方法。

    1
    2
    3
    4
    5
    6
    7
    LeNet(
    (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)
    )
  17. 解决的方法很简单,之前我们在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
    40
    import 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)
  18. 我们用同样的方法调试代码,这里就不赘述了。在进入Sequential的构建,即进入container.py的class Sequential(Module)__init__(self, *args)函数,又面临了和上面第三步一样的条件判断。这次我们当然进入的是if而不是else。对比if和else里面的语句,很容易就能发现刚刚为什么传入元组网络层的索引是序号,而传入有序字典网络层的索引就是名字了。

    image-20200728183810343

  19. 同样通过print(net)可以打印网络的结构,现在所有的网络层都是名字索引了。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    LeNetSequentialOrderDict(
    (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.Sequentialnn.module的容器,用于按顺序包装一组网络层(他的实现过程就是嵌套,比直接定义网络层多了一层封装。nn.Sequential 也是 nn.module的子类,所以initforward时和普通情况是一样的,都是调用同样的nn.module的函数。先是构建Sequential ,然后是Sequential 内的网络层,相当于调用两次的nn.module的函数。)

特点:

  • 顺序性:各网络层之间严格按照顺序构建
  • 自带forward():自带的forward里,通过for循环依次执行前向传播运算

1.2 容器之ModuleList

nn.ModuleList是 nn.module的容器,用于包装一组网络层,以迭代方式调用网络层
主要方法:

  • append():在ModuleList后面添加网络层
  • extend():拼接两个ModuleList
  • insert():指定在ModuleList中位置插入网络层

代码如下:

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
import torch
import torchvision
import torch.nn as nn
from collections import OrderedDict

class ModuleList(nn.Module):
def __init__(self):
super(ModuleList, self).__init__()
self.linears = nn.ModuleList([nn.Linear(10, 10) for i in range(20)])

def forward(self, x):
for i, linear in enumerate(self.linears):
x = linear(x)
return x


net = ModuleList()

print(net)

fake_data = torch.ones((10, 10))

output = net(fake_data)

print(output)

实验:

  1. 打断点,进入

    image-20200728191310802

  2. 跳转到class ModuleList(nn.Module)__init__(self)函数。这里用了列表解析式生成20层10神经元的全连接层。

    image-20200728191703756

  3. 进入会跳转到container.py的class ModuleList(Module)__init__(self, modules=None)函数,可以发现这就是列表的拼接方法。所以ModuleList就是将网络层像列表一样拼接。

    image-20200728191917047

  4. 完成模型初始化跳出到主函数,可以看到net有linears属性,linears是一个ModuleList类。同样ModuleList类也是继承于nn.Module类的,所以linears也有8个有序字典属性。

    image-20200728192117057

    image-20200728192258724

  5. 运行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
    24
    oduleList(
    (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)
    )
    )
  6. 运行到output = net(fake_data),进入,观察前向传播过程。方法和之前一样,不再赘述。前向传播是在class ModuleList(nn.Module)forward中实现的。

    image-20200728200855120

可见,使用ModuleList可以很方便的实现重复的网络结构。

1.3 容器之ModuleLDict

nn.ModuleDict是 nn.module的容器,用于包装一组网络层,以索引方式调用网络层
主要方法:

  • clear():清空ModuleDict
  • items():返回可迭代的键值对(key-value pairs)
  • keys():返回字典的键(key)
  • values():返回字典的值(value)
  • pop():返回一对键值,并从字典中删除

代码如下:

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
import torch
import torchvision
import torch.nn as nn
from collections import OrderedDict

class ModuleDict(nn.Module):
def __init__(self):
super(ModuleDict, self).__init__()
self.choices = nn.ModuleDict({
'conv': nn.Conv2d(10, 10, 3),
'pool': nn.MaxPool2d(3)
})

self.activations = nn.ModuleDict({
'relu': nn.ReLU(),
'prelu': nn.PReLU()
})

def forward(self, x, choice, act):
x = self.choices[choice](x)
x = self.activations[act](x)
return x


net = ModuleDict()

fake_img = torch.randn((4, 10, 32, 32))

output = net(fake_img, 'conv', 'relu')

print(output)
  1. self.choices = nn.ModuleDict中定义了卷积层和池化层,在self.activations = nn.ModuleDict中定义了两种激活函数。其实就是定义了两个字典,一个是self.choices,一个是self.activations,通过键(如’conv’)就可以访问对应的值(如nn.Conv2d(10, 10, 3))。
  2. output = net(fake_img, ‘conv’, ‘relu’)就是使用卷积层加relu激活函数。

实验:

  1. 打断点进入。

    image-20200728204822310

  2. 跳转到class ModuleDict(nn.Module)__init__,运行到最后进入container.py的class ModuleDict(Module)__init__(self, modules=None)

    image-20200728204938892

  3. 可以看到,这和ModuleList的结构非常相似,这里用了self.update函数向字典内添加元素。这就是ModuleList的初始化方法。

    image-20200728205218955

    image-20200728205721570

  4. 再看看它的前向传播实现。调试方法不再赘述,最终跳转到class ModuleDict(nn.Module)forward函数,前向传播就是在这里实现的。

    image-20200728210215306

  5. 进入self.choices[choice](x),跳转到container.py的class ModuleDict(Module)__getitem__(self, key),在这里实现从ModuleDict_modules属性中索引出我们需要的网络层。运行完会跳回class ModuleDict(nn.Module)forward函数,继续完成前向传播。

    image-20200728210643588

  6. 最终得到了输出。

    image-20200728211010393

1.4 容器总结

  • nn.Sequential:顺序性,各网络层之间严格按顺序执行,常用于block构建
  • nn.ModuleList:迭代性,常用于大量重复网构建,通过for循环实现重复构建
  • nn.ModuleDict:索引性,常用于可选择的网络层

2. AlexNet构建

学完了模型的构建以及容器,我们观察pytorch的torchvision模块中提供的AlexNet的构建。

image-20200728211551299

实例化AlexNet的代码:

1
alexnet = torchvision.models.AlexNet()

进入alexnet.py中,代码如下:

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
class AlexNet(nn.Module):

def __init__(self, num_classes=1000):
super(AlexNet, self).__init__()
self.features = nn.Sequential(
nn.Conv2d(3, 64, kernel_size=11, stride=4, padding=2),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2),
nn.Conv2d(64, 192, kernel_size=5, padding=2),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2),
nn.Conv2d(192, 384, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(384, 256, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(256, 256, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2),
)
self.avgpool = nn.AdaptiveAvgPool2d((6, 6))
self.classifier = nn.Sequential(
nn.Dropout(),
nn.Linear(256 * 6 * 6, 4096),
nn.ReLU(inplace=True),
nn.Dropout(),
nn.Linear(4096, 4096),
nn.ReLU(inplace=True),
nn.Linear(4096, num_classes),
)

def forward(self, x):
x = self.features(x)
x = self.avgpool(x)
x = torch.flatten(x, 1)
x = self.classifier(x)
return x

可以看到,它使用了sequential的方法构建网络。

此外torchvision.models中还预设了许多其他网络,可以自行查看。

image-20200728212249165