上篇文章学习了如何搭建一个模型。搭建一个模型需要两个要素,第一是构建子模型,第二是拼接子模型。在搭建模型时,还有一个非常重要的属性是模型容器

  1. 网络层容器(Containers)
  2. AlexNet构建

一、网络层容器

模型容器 与 AlexNet构建 - 图1

01. nn.Sequential

nn.Sequentialnn.module 的容器,用于按顺序包装一组网络层,将这组网络层看成一个整体。也可以看作是模型当中的一个子模块。

在传统的机器学习当中,有一部叫做特征工程,我们需要人为的去设计特征,然后将特征输入的分类器去分类。在深度学习时代已经弱化了特征工程这一个概念,尤其是卷积神经网络对图像的特征完全不需要人为去设计,这一切都可以交给卷积神经网络自动去学习特征。然后在最后会加上这个全连接层,用于输出分类结果。在早期阶段的神经网络当中用于分类的分类器是由全连接层构成的,所以在深度学习时代也有习惯以全连接层分界线。将网络模型一个特征提取模块和分类模块,对一个大的模型进行划分,以便于在模型进行管理。利用我们的LeNe模型,可以将前面的卷积、池化进行一个包装,把它称为特征提取器。最后把全连接层的部分进行包装,称为一个分类器classifier。对这两个部分在统一的包装构成了LeNet整体网络。

在PyTorch中,可以采用 Sequential 这个容器,将前面四层卷积、池化包装成一个 Sequential ;将三层全连接层包装成一个 Sequential ,组装这两个 Sequential 构成整体的LeNet。下面通过代码来观察使用 Sequential 构建LeNet。

  1. # ============================ Sequential ============================ #
  2. class LeNetSequential(nn.Module):
  3. def __init__(self, classes):
  4. super(LeNetSequential, self).__init__()
  5. self.features = nn.Sequential(
  6. nn.Conv2d(3, 6, 5),
  7. nn.ReLU(),
  8. nn.MaxPool2d(kernel_size=2, stride=2),
  9. nn.Conv2d(6, 16, 5),
  10. nn.ReLU(),
  11. nn.MaxPool2d(kernel_size=2, stride=2),)
  12. self.classifier = nn.Sequential(
  13. nn.Linear(16*5*5, 120),
  14. nn.ReLU(),
  15. nn.Linear(120, 84),
  16. nn.ReLU(),
  17. nn.Linear(84, classes),)
  18. def forward(self, x):
  19. x = self.features(x)
  20. x = x.view(x.size()[0], -1)
  21. x = self.classifier(x)
  22. return x
  23. net = LeNetSequential(classes=2)
  24. fake_img = torch.randn((4, 3, 32, 32), dtype=torch.float32)
  25. output = net(fake_img)
  26. print(net)
  27. print(output)

下面我们来观察一下 Sequential 中他实现了一个什么功能。这样我们单步运行到上面代码的第11行,现在采用 step into 进入到 MaxPool2d 类中,然后我们step out 之后,在step into,现在来到了 container.py 文件中的 Sequential类 ,我们看到其还是继承 Module类 。既然他继承于 Module类,所以 Sequential类 还是会有8个有序字典去管理他的属性。还是调用了 Module 父类的 __init__类 去构建相应的属性。

  1. class Sequential(Module):
  2. def __init__(self, *args):
  3. super(Sequential, self).__init__()
  4. if len(args) == 1 and isinstance(args[0], OrderedDict):
  5. for key, module in args[0].items():
  6. self.add_module(key, module)
  7. else:
  8. for idx, module in enumerate(args):
  9. self.add_module(str(idx), module)

这样我们来看一下这个 self Sequential() 的8个有序字典。接下来会对我们传入的这些网络层参数添加到我们的Sequential 当中。看到这里会做一个判断,按照我们输入的参数的数据类型看是否是一个有序字典。这里应该不是有序字典,所以会跳到else,进入一个for循环,对传入参数进行循环取出每一个网络层,然后采用 add_module方法 将网络层添加到 Sequential 当中。
image.png
经过不断的循环添加网络层,就完成了一个 Sequential 的构建。现在回到LeNet这个类当中。你们知道LeNet的 _modules_parameters 字典还是空的。因为截至目前,我们的程序只是实现了等号右边 nn.Sequential 的构建。还没有把 nn.Sequential 复制给我们的类型属性 self.features 。当我们赋值完成之后,我们才会在 _modules 字典中看到 Sequential
image.png
看到在self下的 _modules 字典中出现了 features 这个key,key里头的value就是等号右边这个 Sequential ,我们点开 Sequential ,他又是一个 Module ,所以他还有这8个有序字典。接着我们看到在 features 中的 _modules 有刚刚构建的这6个网络层,这六个网络层是顺序构建的。

分类层同理,这样采用 Sequential 构建网络就完成了
image.png
目前为止,构建子模块流程完成,下面我们看一下 PyTorch 是如何拼接各个子模块的。在 LeNetSequential类 中的第21行下断点并进入。进入到的是 module.py 中的 __call__函数

  1. def __call__(self, *input, **kwargs):
  2. for hook in self._forward_pre_hooks.values():
  3. result = hook(self, input)
  4. if result is not None:
  5. if not isinstance(result, tuple):
  6. result = (result,)
  7. input = result
  8. if torch._C._get_tracing_state():
  9. result = self._slow_forward(*input, **kwargs)
  10. else:
  11. result = self.forward(*input, **kwargs)
  12. for hook in self._forward_hooks.values():
  13. hook_result = hook(self, input, result)
  14. if hook_result is not None:
  15. result = hook_result
  16. if len(self._backward_hooks) > 0:
  17. var = result
  18. while not isinstance(var, torch.Tensor):
  19. if isinstance(var, dict):
  20. var = next((v for v in var.values() if isinstance(v, torch.Tensor)))
  21. else:
  22. var = var[0]
  23. grad_fn = var.grad_fn
  24. if grad_fn is not None:
  25. for hook in self._backward_hooks.values():
  26. wrapper = functools.partial(hook, self)
  27. functools.update_wrapper(wrapper, hook)
  28. grad_fn.register_hook(wrapper)
  29. return result

跳转到上面代码的第11行(具体在源码中的位置,自己对应)。然后step into ,就会进入到 LeNetSequential类 中的 forward 函数。然后在点击step into,跳转到 container.py 中的 Sequential类 中的 forward函数

  1. class Sequential(Module):
  2. ...
  3. def forward(self, input):
  4. for module in self:
  5. input = module(input)
  6. return input

Sequential 中的网络层进行前向传播。
image.png
上图是将Conv2d进行前向运算,对input实现了卷积操作。这样迭代的从Sequential中取出网络层进行前向运算。由于是按照严格的顺序进行前向运算,所以数据格式、类型必须要统一。
最终的输出结果如下图所示:
image.png
但是从上图看出,各个网络层没有对应的名称,只能通过序号进行索引,如果是个很大的网络,很难通过序号进行索引。所以我们可以通过对网络层命名进行索引。

nn.Sequential() 传入有序字典 OrderDict({ , , , }) ,以这种方式来构建我们的网络。

  1. class LeNetSequentialOrderDict(nn.Module):
  2. """
  3. 给网络层命名
  4. """
  5. def __init__(self, classes):
  6. super(LeNetSequentialOrderDict, self).__init__()
  7. self.features = nn.Sequential(OrderedDict({
  8. 'conv1': nn.Conv2d(3, 6, 5),
  9. 'relu1': nn.ReLU(inplace=True),
  10. 'pool1': nn.MaxPool2d(kernel_size=2, stride=2),
  11. 'conv2': nn.Conv2d(6, 16, 5),
  12. 'relu2': nn.ReLU(inplace=True),
  13. 'pool2': nn.MaxPool2d(kernel_size=2, stride=2),
  14. }))
  15. self.classifier = nn.Sequential(OrderedDict({
  16. 'fc1': nn.Linear(16*5*5, 120),
  17. 'relu3': nn.ReLU(),
  18. 'fc2': nn.Linear(120, 84),
  19. 'relu4': nn.ReLU(inplace=True),
  20. 'fc3': nn.Linear(84, classes),
  21. }))
  22. def forward(self, x):
  23. x = self.features(x)
  24. x = x.view(x.size()[0], -1)
  25. x = self.classifier(x)
  26. return x
  27. net = LeNetSequentialOrderDict(classes=2)
  28. fake_img = torch.randn((4, 3, 32, 32), dtype=torch.float32)
  29. output = net(fake_img)
  30. print(net)
  31. print(output)

按照同样的调试步骤,在34行下断点,进入 LeNetSequentialOrderDict类 ,然后在15行进行两次step into,进入 container.py 中的 Sequential类

  1. class Sequential(Module):
  2. def __init__(self, *args):
  3. super(Sequential, self).__init__()
  4. if len(args) == 1 and isinstance(args[0], OrderedDict):
  5. for key, module in args[0].items():
  6. self.add_module(key, module)
  7. else:
  8. for idx, module in enumerate(args):
  9. self.add_module(str(idx), module)

因为传入的是OrderDict,所以进入第一个判断中。对Dict字典中中的key和value取出并加入到module中。key就是网络层的名称,value为网络层module。
image.png

总结:

nn.Sequentialnn.module 的容器,用于按顺序包装一组网络层

  • 顺序性:各网络层之间严格按照顺序构建 ,一定要保证前后的数据关系一致。
  • 自带forward():自带的forward函数,通过for循环依次执行前向传播运算

02. nn.ModuleList

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

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

    ============================ ModuleList ============================

    class ModuleList(nn.Module): def init(self):

    1. super(ModuleList, self).__init__()
    2. 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)

在第13行加上断点,进入到 `ModuleList类` 中,运行到第5行,执行20次循环列表生成之后,setp into到 `container.py` **文件中的 **`ModuleList``类` 中的 `__init__函数`。
```python
class ModuleList(Module):
    def __init__(self, modules=None):
        super(ModuleList, self).__init__()
        if modules is not None:
            self += modules

image.png
image.png

03. nn.ModuleDict

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

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

    ============================ ModuleDict ============================

    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)

<a name="5VZzV"></a>
## 04. 总结

- `nn.Sequential` :**顺序性,**各网络层之间严格按顺序执行,常用于block构建
- `nn.ModuleList` :**迭代性,**常用于大量重复网构建,通过for循环实现重复构建
- `nn.ModuleDict` :**索引性,**常用于可选择的网络层
<a name="wjNys"></a>
# 二、AlexNet搭建
```python
# torchvision.models.AlexNet()
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