[PyTorch 學習筆記] 3.1 模型建立步驟與 nn.Module

本章代碼:https://github.com/zhangxiann/PyTorch_Practice/blob/master/lesson3/module_containers.pypython


這篇文章來看下 PyTorch 中網絡模型的建立步驟。網絡模型的內容以下,包括模型建立和權值初始化,這些內容都在nn.Module中有實現。git


網絡模型的建立步驟

建立模型有 2 個要素:構建子模塊拼接子模塊。如 LeNet 裏包含不少卷積層、池化層、全鏈接層,當咱們構建好全部的子模塊以後,按照必定的順序拼接起來。github


這裏以上一篇文章中 `lenet.py`的 LeNet 爲例,繼承`nn.Module`,必須實現`__init__()` 方法和`forward()`方法。其中`__init__()` 方法裏建立子模塊,在`forward()`方法裏拼接子模塊。
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

當咱們調用net = LeNet(classes=2)建立模型時,會調用__init__()方法建立模型的子模塊。api

當咱們在訓練時調用outputs = net(inputs)時,會進入module.pycall()函數中:網絡

def __call__(self, *input, **kwargs):
        for hook in self._forward_pre_hooks.values():
            result = hook(self, input)
            if result is not None:
                if not isinstance(result, tuple):
                    result = (result,)
                input = result
        if torch._C._get_tracing_state():
            result = self._slow_forward(*input, **kwargs)
        else:
            result = self.forward(*input, **kwargs)
        ...
        ...
        ...

最終會調用result = self.forward(*input, **kwargs)函數,該函數會進入模型的forward()函數中,進行前向傳播。app

torch.nn中包含 4 個模塊,以下圖所示。框架


其中全部網絡模型都是繼承於`nn.Module`的,下面重點分析`nn.Module`模塊。

nn.Module

nn.Module 有 8 個屬性,都是OrderDict(有序字典)。在 LeNet 的__init__()方法中會調用父類nn.Module__init__()方法,建立這 8 個屬性。less

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 屬性:存儲管理 nn.Parameter 類型的參數
  • _modules 屬性:存儲管理 nn.Module 類型的參數
  • _buffers 屬性:存儲管理緩衝屬性,如 BN 層中的 running_mean
  • 5 個 ***_hooks 屬性:存儲管理鉤子函數

其中比較重要的是parametersmodules屬性。機器學習

在 LeNet 的__init__()中建立了 5 個子模塊,nn.Conv2d()nn.Linear()都是 繼承於nn.module,也就是說一個 module 都是包含多個子 module 的。ide

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)
        ...
        ...
        ...

當調用net = LeNet(classes=2)建立模型後,net對象的 modules 屬性就包含了這 5 個子網絡模塊。


下面看下每一個子模塊是如何添加到 LeNet 的`_modules` 屬性中的。以`self.conv1 = nn.Conv2d(3, 6, 5)`爲例,當咱們運行到這一行時,首先 Step Into 進入 `Conv2d`的構造,而後 Step Out。右鍵`Evaluate Expression`查看`nn.Conv2d(3, 6, 5)`的屬性。

上面說了`Conv2d`也是一個 module,裏面的`_modules`屬性爲空,`_parameters`屬性裏包含了該卷積層的可學習參數,這些參數的類型是 Parameter,繼承自 Tensor。

此時只是完成了`nn.Conv2d(3, 6, 5)` module 的建立。尚未賦值給`self.conv1 `。在`nn.Module`裏有一個機制,會攔截全部的類屬性賦值操做(`self.conv1`是類屬性),進入到`__setattr__()`函數中。咱們再次 Step Into 就能夠進入`__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
            ...
            ...
            ...

在這裏判斷 value 的類型是Parameter仍是Module,存儲到對應的有序字典中。

這裏nn.Conv2d(3, 6, 5)的類型是Module,所以會執行modules[name] = value,key 是類屬性的名字conv1,value 就是nn.Conv2d(3, 6, 5)

總結

  • 一個 module 裏可包含多個子 module。好比 LeNet 是一個 Module,裏面包括多個卷積層、池化層、全鏈接層等子 module
  • 一個 module 至關於一個運算,必須實現 forward() 函數
  • 每一個 module 都有 8 個字典管理本身的屬性

模型容器

除了上述的模塊以外,還有一個重要的概念是模型容器 (Containers),經常使用的容器有 3 個,這些容器都是繼承自nn.Module

  • nn.Sequetial:按照順序包裝多個網絡層
  • nn.ModuleList:像 python 的 list 同樣包裝多個網絡層,能夠迭代
  • nn.ModuleDict:像 python 的 dict 同樣包裝多個網絡層,經過 (key, value) 的方式爲每一個網絡層指定名稱。

nn.Sequetial

在傳統的機器學習中,有一個步驟是特徵工程,咱們須要從數據中認爲地提取特徵,而後把特徵輸入到分類器中預測。在深度學習的時代,特徵工程的概念被弱化了,特徵提取和分類器這兩步被融合到了一個神經網絡中。在卷積神經網絡中,前面的卷積層以及池化層能夠認爲是特徵提取部分,然後面的全鏈接層能夠認爲是分類器部分。好比 LeNet 就能夠分爲特徵提取分類器兩部分,這 2 部分均可以分別使用 nn.Seuqtial 來包裝。


代碼以下:
class LeNetSequetial(nn.Module):
    def __init__(self, classes):
        super(LeNet2, self).__init__()
        self.features = nn.Sequential(
            nn.Conv2d(3, 6, 5),
            nn.ReLU(),
            nn.MaxPool2d(2, 2),
            nn.Conv2d(6, 16, 5),
            nn.ReLU(),
            nn.MaxPool2d(2, 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

在初始化時,nn.Sequetial會調用__init__()方法,將每個子 module 添加到 自身的_modules屬性中。這裏能夠看到,咱們傳入的參數能夠是一個 list,或者一個 OrderDict。若是是一個 OrderDict,那麼則使用 OrderDict 裏的 key,不然使用數字做爲 key (OrderDict 的狀況會在下面說起)。

def __init__(self, *args):
        super(Sequential, self).__init__()
        if len(args) == 1 and isinstance(args[0], OrderedDict):
            for key, module in args[0].items():
                self.add_module(key, module)
        else:
            for idx, module in enumerate(args):
                self.add_module(str(idx), module)

網絡初始化完成後有兩個子 modulefeaturesclassifier


而`features`中的子 module 以下,每一個網絡層以序號做爲 key:

在進行前向傳播時,會進入 LeNet 的`forward()`函數,首先調用第一個`Sequetial`容器:`self.features`,因爲`self.features`也是一個 module,所以會調用`__call__()`函數,裏面調用

result = self.forward(*input, **kwargs),進入nn.Seuqetialforward()函數,在這裏依次調用全部的 module。

def forward(self, input):
        for module in self:
            input = module(input)
        return input

在上面能夠看到在nn.Sequetial中,裏面的每一個子網絡層 module 是使用序號來索引的,即便用數字來做爲 key。一旦網絡層增多,難以查找特定的網絡層,這種狀況可使用 OrderDict (有序字典)。代碼中使用

class LeNetSequentialOrderDict(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),
        }))
        ...
        ...
        ...

總結

nn.Sequetialnn.Module的容器,用於按順序包裝一組網絡層,有如下兩個特性。

  • 順序性:各網絡層之間嚴格按照順序構建,咱們在構建網絡時,必定要注意先後網絡層之間輸入和輸出數據之間的形狀是否匹配
  • 自帶forward()函數:在nn.Sequetialforward()函數裏經過 for 循環依次讀取每一個網絡層,執行前向傳播運算。這使得咱們咱們構建的模型更加簡潔

nn.ModuleList

nn.ModuleListnn.Module的容器,用於包裝一組網絡層,以迭代的方式調用網絡層,主要有如下 3 個方法:

  • append():在 ModuleList 後面添加網絡層
  • extend():拼接兩個 ModuleList
  • insert():在 ModuleList 的指定位置中插入網絡層

下面的代碼經過列表生成式來循環迭代建立 20 個全鏈接層,很是方便,只是在 forward()函數中須要手動調用每一個網絡層。

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)

nn.ModuleDict

nn.ModuleDictnn.Module的容器,用於包裝一組網絡層,以索引的方式調用網絡層,主要有如下 5 個方法:

  • clear():清空 ModuleDict
  • items():返回可迭代的鍵值對 (key, value)
  • keys():返回字典的全部 key
  • values():返回字典的全部 value
  • pop():返回一對鍵值,並從字典中刪除

下面的模型建立了兩個ModuleDictself.choicesself.activations,在前向傳播時經過傳入對應的 key 來執行對應的網絡層。

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')
# output = net(fake_img, 'conv', 'prelu')
print(output)

容器總結

  • nn.Sequetial:順序性,各網絡層之間嚴格按照順序執行,經常使用於 block 構建,在前向傳播時的代碼調用變得簡潔
  • nn.ModuleList:迭代行,經常使用於大量重複網絡構建,經過 for 循環實現重複構建
  • nn.ModuleDict:索引性,經常使用於可選擇的網絡層

PyTorch 中的 AlexNet

AlexNet 是 Hinton 和他的學生等人在 2012 年提出的卷積神經網絡,以高出第二名 10 多個百分點的準確率得到 ImageNet 分類任務冠軍,今後卷積神經網絡開始在世界上流行,是劃時代的貢獻。

AlexNet 特色以下:

  • 採用 ReLU 替換飽和激活 函數,減輕梯度消失
  • 採用 LRN (Local Response Normalization) 對數據進行局部歸一化,減輕梯度消失
  • 採用 Dropout 提升網絡的魯棒性,增長泛化能力
  • 使用 Data Augmentation,包括 TenCrop 和一些色彩修改

AlexNet 的網絡結構能夠分爲兩部分:features 和 classifier。


在`PyTorch`的計算機視覺庫`torchvision.models`中的 AlexNet 的代碼中,使用了`nn.Sequential`來封裝網絡層。
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

參考資料


若是你以爲這篇文章對你有幫助,不妨點個贊,讓我有更多動力寫出好文章。

相關文章
相關標籤/搜索