1. 网络模型创建步骤
  2. nn.Module 属性

在前面文章中主要学习了数据模块。在数据模块中我们了解了pytorch是如何从硬盘当中去读取数据,然后对数据进行预处理,数据增强,最后转换为张量的形式注入到我们的模型当中。深度学习模型是对数据进行复杂的数学运算,最终得到我们可以进行分类分割,目标检测的输出,那么模型当中到底是什么样的呢?今天我们就来学习模型的创建,以及 nn.Module 的概念。

在学习模型创建步骤之前,我们再来回顾一下机学习模型训练的五大步骤。第一是数据模块,第二是模型模块,第三是损失函数,第四是优化器。第五迭代训练。第二章已经把数据模块讲解完毕,接下来我们来学习模型模块。

一、模型的创建步骤

模型创建 与 nn.Module - 图1模型模块分为两个子模块,一个是构建网络层,第二个是权值初始化。在模型创建中又分为两个子模块,一个是构建网络层,第二个是拼接网络层。例如我们卷积神经网络当中有卷积层,池化层,激活函数层以及全连接层等。我们首先要构建这些子模块,构建好这些网络层之后,再把它按照一定的顺序 一定的拓扑结构进行拼接。拼接起来之后就可以得到我们复杂的神经网络。这两步我们就要构建好了一个模型,创建好的一个模型之后,我们要对模型的权值进行初始化。初始化当中pytorch提供了丰富的初始化方法。

以上这一切都会基于 nn.Module 这一个概念。nn.Module 整个模型模块的一个根基。

下面我们就来学习如何创建一个模型。其实我们在人民币二分类实验当中就已经创建过了一个卷积神经网络模型LeNet。我们来回顾一下这一个模型。这个是LeNet的模型结构示意图。我们看到里面有很多网络层构成,2层卷积、2个池化、3个全连接层,一共是七层构成。
模型创建 与 nn.Module - 图2
在创建 LeNet 的时候,我们首先会构建这些子网络层、子模块。构建好这7个网络层之后,把它们进行按一定的顺序进行连接并包装起来,就可以得到 LeNet 网络模型。在PyTorch中,我们LeNet是一个module的概念。而其中的卷积层、池化层、全连接层也是一个model的概念。它们都属于 nn.Module 这一个类。所以从这里我们知道一个 nn.Module ,比如说LeNet可以包含很多个子模块,比如的网络层,卷积层,池化层等等。下面我们从计算图的角度来观察模型的创建,以及我们模型当中会实现什么东西。
image.png
上图是一个LeNet计算图。在计算图当中有两个主要的概念,一个是节点,一个是边。节点就是我们的tensor数据,上图中的彩色的方块代表数据。边就是运算,上图中的箭头代表运算。

我们来看一下整个LeNet,他其实可以看成是一个大的运算。LeNet接受一个模型创建 与 nn.Module - 图4的张量,经过一系列复杂的运算之后呢,输出一个长度为10的一个向量,作为我们的分类概率。LeNet可以充当一个复杂的运算,它是由很多子网络层构成的。前一层经过运算,其输出可以输入到下一个网络层中,那下一个网络层再执行一个运算,这样不断的前向传播,最终得到我们的输出概率。

我们通过网络模型的结构、以及计算图这两个角度去分析了一个模型。从这里我们知道构建模型应该有两个要素,第一是构建子模块。例如我们要构建的LeNet就是由很多个子网络层构成的。所以我们首先要构建卷积层、池化层、全连接层这些子网络层之后,再把这些子网络层按照一定的顺序、一定的拓扑结构进行拼接之后,我就可以得到了模型。下面我们通过代码来学习模型创建的步骤。

  1. class LeNet(nn.Module):
  2. def __init__(self, classes):
  3. """构建子模块
  4. """
  5. super(LeNet, self).__init__()
  6. self.conv1 = nn.Conv2d(3, 6, 5)
  7. self.conv2 = nn.Conv2d(6, 16, 5)
  8. self.fc1 = nn.Linear(16 * 5 * 5, 120)
  9. self.fc2 = nn.Linear(120, 84)
  10. self.fc3 = nn.Linear(84, classes)
  11. def forward(self, x):
  12. """拼接子模块,前向传播
  13. """
  14. out = F.relu(self.conv1(x))
  15. out = F.max_pool2d(out, 2)
  16. out = F.relu(self.conv2(out))
  17. out = F.max_pool2d(out, 2)
  18. out = out.view(out.size(0), -1)
  19. out = F.relu(self.fc1(out))
  20. out = F.relu(self.fc2(out))
  21. out = self.fc3(out)
  22. return out

模型创建 与 nn.Module - 图5

二、 nn.Module()

模型创建 与 nn.Module - 图6在模型模块当中,有个非常重要的点 nn.Module() ,所有的网络层都是继承这个类。 torch.nn 是神经网络模块,在其中也有很多子模块,这里需要了解4个。

  1. nn.Parameter 是一个张量的子类,通常用来表示可学习的参数,例如权值或者是bias。
  2. 第二个是 nn.Module 。他是所有网络层的基类,用来管理网络的属性。我们所有的模型,比如 LeNet 是一个 nn.Module类 ,里面的子模块:卷积层、池化层等也是一个 nn.model类
  3. 第三个是 nn.functional ,是函数的具体实现。
  4. 第四个是 nn.init 。提供了丰富的初始化的方法。这个在后面我们会觉得学习这个初始化。

今天我们的重点是 nn.Module :所有网络层的基类。我们来看 nn.Module 还有哪些属性。在 nn.Module 中有8个重要的有序字典,八个重要的属性用于管理整个模型。

  1. self._parameters = OrderedDict()
  2. self._buffers = OrderedDict()
  3. self._backward_hooks = OrderedDict()
  4. self._forward_hooks = OrderedDict()
  5. self._forward_pre_hooks = OrderedDict()
  6. self._state_dict_hooks = OrderedDict()
  7. self._load_state_dict_pre_hooks = OrderedDict()
  8. self._modules = OrderedDict()

这里着重关注两个 _parameters_modules

  • _parameters : 存储管理 nn.Parameter
  • _modules : 存储管理 nn.Module
  • _buffers :存储管理缓冲属性,如BN层中的running_mean
  • ***_hooks :存储管理钩子函数

下面通过代码来观察 nn.Module 的创建以及对属性管理的机制。我们继续采用LeNet模型,我们来观察 Module 的一个创建。继续debug。
我们在 net = LeNet(classes=2) 改行设置断点,然后我们进入 LeNet ,我们看到LeNet是继承于 nn.Module 。所以LeNet是一个 module 。然后 super 实现一个父类的函数调用的功能。LeNet的父类是 nn.Module ,所以他调用的是父类 __init__函数 ,进入到 module.py , __init__函数 实现了我们8个有序字典的一个初始化。

class Module(object):
    r"""Base class for all neural network modules.

    Your models should also subclass this class.

    Modules can also contain other Modules, allowing to nest them in
    a tree structure. You can assign the submodules as regular attributes::

        import torch.nn as nn
        import torch.nn.functional as F

        class Model(nn.Module):
            def __init__(self):
                super(Model, self).__init__()
                self.conv1 = nn.Conv2d(1, 20, 5)
                self.conv2 = nn.Conv2d(20, 20, 5)

            def forward(self, x):
                x = F.relu(self.conv1(x))
                return F.relu(self.conv2(x))

    Submodules assigned in this way will be registered, and will have their
    parameters converted too when you call :meth:`to`, etc.
    """

    dump_patches = False

    r"""This allows better BC support for :meth:`load_state_dict`. In
    :meth:`state_dict`, the version number will be saved as in the attribute
    `_metadata` of the returned state dict, and thus pickled. `_metadata` is a
    dictionary with keys that follow the naming convention of state dict. See
    ``_load_from_state_dict`` on how to use this information in loading.

    If new parameters/buffers are added/removed from a module, this number shall
    be bumped, and the module's `_load_from_state_dict` method can compare the
    version number and do appropriate changes if the state dict is from before
    the change."""
    _version = 1

    def __init__(self):
        """
        Initializes internal Module state, shared by both nn.Module and ScriptModule.
        """
        torch._C._log_api_usage_once("python.nn_module")

        self.training = True  # 表示模型的训练状态
        self._parameters = OrderedDict()
        self._buffers = OrderedDict()
        self._backward_hooks = OrderedDict()
        self._forward_hooks = OrderedDict()
        self._forward_pre_hooks = OrderedDict()
        self._state_dict_hooks = OrderedDict()
        self._load_state_dict_pre_hooks = OrderedDict()
        self._modules = OrderedDict()

这里我们主要关注 _parameters_modules 。跳回LeNet.py文件,我们就构建好了一个module的基本属性。
image.png
在控制台中可以看到 self 下有了8个有序字典,现在这些字典当中都是空的。接着我们开始进行构建子模块。这里我们第一个子网络层是 Conv2d 卷积层。我们现在进入 Conv2d 去观察。Conv2d 类继承 _ConvNd 类。首先要进行参数的获取,之后还是调用 super 这个方法去调用它的父类的 __init__
image.png
现在我们进入其父类去看一下。这里会进入这一个 _pair函数 ,这是我们我们 step out 之后再进入就可以来到了_ConvNd__init 当中。我们看到_ConvNd 也是继承于 Module 的。
image.png
所以 Conv2d 还是一个 module 。这里我们看到还是用了一个super去调用父类 Module__init__ ,其实就是去为了构建那8个有序字典。接着进入 __init__ 方法,还是跳入了module.py中的 Module
image.png
这里之后我就直接单步跳出了。我们看到现在我们第一个网络层就初始化并构建完毕。构建好之后,我们LeNet就会记录这一个卷积层,由于卷积层属于一个 module ,所以它在 _Modules 这一个字典当中我们看到。我们的 self 这个字典中就有了一个卷积层conv1, Conv2d 继承 _ConvNd_ConvNd 继承 Module ,所以我们的卷积层conv1也是一个 Module 。既然conv1是一个 Module ,肯定也会有我们这8个有序字典,但是我们发现这个卷积层当中的 _modules 是空的,这是由于我们卷积层已经没有子网络,他只有一些可学习参数,在 _parameters 中可以查看。
image.png
下面我们通过构建第二个卷积层来观察如何将 子Module 存储到 _modules 字典中的这一个机制。我们进入到 self.conv2 = nn.Conv2d(6, 16, 5) 中,这样我们继续采用 step into 进入到这一个方法中去构建第二个卷积层。这里进入的是 Conv2d 里是卷积层构建,我们暂时不需要关心。我们现在直接可以跳出这个 __init__函数 。到这里我们发现。我们的 _modules 字典中还没有出现 conv2 这个属性。这里只是实现 Conv2d 的一个实例化。到现在还没有赋值到我们这个LeNet的类属性 conv2 当中,只是构建了这一个网络层。
image.png
下一步才要赋值到 self.conv2 的属性中。为什么要强调这一个赋值呢?这是因为现在我们还是不能直接将它复制到 self.conv2 这个属性当中。因为在 Module 里面有一个机制,该机制就是setattr 函数,该函数会拦截所有类属性赋值操作语句。跳转到 Module.py 中的setattr函数。

    def __setattr__(self, name, value):
        def remove_from(*dicts):
            for d in dicts:
                if name in d:
                    del d[name]

        params = self.__dict__.get('_parameters')
        if isinstance(value, Parameter):
            if params is None:
                raise AttributeError(
                    "cannot assign parameters before Module.__init__() call")
            remove_from(self.__dict__, self._buffers, self._modules)
            self.register_parameter(name, value)
        elif params is not None and name in params:
            if value is not None:
                raise TypeError("cannot assign '{}' as parameter '{}' "
                                "(torch.nn.Parameter or None expected)"
                                .format(torch.typename(value), name))
            self.register_parameter(name, value)
        else:
            modules = self.__dict__.get('_modules')
            if isinstance(value, Module):
                if modules is None:
                    raise AttributeError(
                        "cannot assign module before Module.__init__() call")
                remove_from(self.__dict__, self._parameters, self._buffers)
                modules[name] = value
            elif modules is not None and name in modules:
                if value is not None:
                    raise TypeError("cannot assign '{}' as child module '{}' "
                                    "(torch.nn.Module or None expected)"
                                    .format(torch.typename(value), name))
                modules[name] = value
            else:
                buffers = self.__dict__.get('_buffers')
                if buffers is not None and name in buffers:
                    if value is not None and not isinstance(value, torch.Tensor):
                        raise TypeError("cannot assign '{}' as buffer '{}' "
                                        "(torch.Tensor or None expected)"
                                        .format(torch.typename(value), name))
                    buffers[name] = value
                else:
                    object.__setattr__(self, name, value)

这个函数的功能就是被拦截所有类属性的一个赋值操作。那么再来看一下这个机制,我们实例化 Conv2d 之后,还没有进行赋值。我们即将进行赋值的时候,会被拦截下来进入了setattr函数。这个函数的主要作用是什么呢?我们来观察一下。有很多判断语句,我们先判断他是不是 Parameter ,如果是 Parameter ,他就会存储到到 _parameter 这一有序字典当中。
image.png
现在我们先看一下实例化的 value 是一个 Conv2d 。他应该是一个 Module 的一个类型,执行上图中的 if isinstance(value, Module): 我们判断他是否是一个Module,会把 value 存到我们的 modules 当中。我们现在看一下我们的变量。到这里。观察一下我们的LeNet的 _modules 。这个字典当中就有了 conv2 。这样一步一步不断的。每进行一次类属性赋值的时候,就会进入setattr函数,进行判断是否属于 Parameter ,还是属于 Module ,并存储到相对应的字典中。

我们来观察一下我们LeNet。有8个有序字典,我们关注一下modules,就是我们刚刚构建的五个子模块个子网络。我们看到parameters是空的,因为。在构建LeNet子模块并进行属性赋值的时候,我们并没有构建一个Parameter类
image.png
我们再再看一下子网络中的 _parameters 是有权值和bias的。这就是一个 Module 属性了一个构建的一个机制。
image.png
现在我们再来简单回顾一下 nn.Module 属性构建。我们会在 Module类 里进行属性赋值的时候,会被setattr函数去拦截。然后在这一函数当中呢,我们会去判断即将要赋值的这个数据类型是是 nn.parameters类 ,如果是的话他就会存储到 _parameters 这一个字典中。如果他是一个 nn.Module类 ,他就会存储到 _modules 这个字典当中去进行管理。

nn.Module 总结

现在我们来总结一下 nn.Module

  1. 一个 nn.Module 可以包含多个子 module ,例如 LeNet 是一个 大module,包含很多个子module:卷积层、池化层、全连接层。他们也是一个 module ,这些子 module包含在 LeNet 这个大的module当中。
  2. 一个 module 相当于一个运算,必须实现 forward()函数 。这里我们从计算图的角度就可以了解,一个module接受一个张量经过一系列复杂的运算,输出一个分类概率,或者是其他的数据。所以在其中我们要对 module 实现一个forward()函数前向传播的这一个函数。
  3. 第三是每个 module 都有8个字典管理它的属性。这样最常用的就是 _parameters 这一个字典,以及_modules 这个字典。

总结

本篇文章主要学习了模型的创建与nn.Module

模型创建的两个要素,一个是构建子模块主犯,第二是拼接子模块。我们通过搭建一个LeNet模型,学习了 nn.Module 的概念以及其中8个非常重要的属性:8个有序字典。其中有两个非常重要的是_parameters_modules分别用来管理 Module 中模型和可学习参数