PyTorch 中構建模型和輸入数據的方法
PyTorch 中構建模型和輸入数據的方法
最近因為肺炎無法無天只得駐留在家,整理一下早兩年學習的一些資料。
構建模型的方法
最基礎的方法是定義一個類,繼承 nn.Module
,然後在其 def __init__()
中定義(實例化) forward()
中需要使用的網絡层。例如 self.name = nn.Conv2d()
,這個參数名 name 将作为 model 的属性名,可以透過 model.name
訪問,如果這個层是帶有可學習的參数的,那麼它還會有 model.name.weight
或 model.name.bias
等權重属性。
定義完所有不同的层之後,就可以在類的 forward()
函数中使用這些层,並決定輸入数據在網絡中的流動。對於一些不含可訓練參数的层,可以在這裡直接使用 nn.functional 中的函数而無需事先定義。
例如:
1 | import torch |
使用 add_module 方法
在類的初始化函式中,self.name = nn.Module()
等同於調用 self.add_module("name", nn.Module())
,后者這種方法主要是當层比較多而都是重複的結構時(例如 ResNet 中很多殘差單元都是完全相同的),方便自動設置层的名称。對應的在 forward 中常使用 getattr(self, "name")(x)
。
例如(代碼不完全):
1 | def __init__(self): |
使用 nn.Sequential,以及更一般的情況
需要進行一些判斷(以重複使⽤)的层或其它情況,可以将各種层 append 到一個 list 中,然后使⽤解構賦值 self.name = nn.Sequential(*[Conv2d(), Conv2d(), ...])
。這樣的层的名稱为数字编号,访问其中的层: model.name[index]
。給 nn.Sequential()
內的层賦上名稱,可以使用 from collections import OrderedDict
-> nn.Sequential(OrderedDict([("name1", layer1()), ("name2", layer2())]))
,加上了名稱之後就可以使用 .
的方法訪問了。
nn.Sequential
實際上就是一個繼承了 nn.Module
的類,如同最開頭那樣定義的。因此我們可以直接單獨使用它:
1 | model = nn.Sequential( |
既然可以在自定義的類(A)中的 init 初始化 nn.Sequential
,那麼也可以初始化另一個自定義類(B),當然 nn.Sequential
中也可以添加一個自定義類,最終裡面的层的名稱逐級嵌套,類似於 model.name_in_netA.name_in_netB[index in Sequential]
。
更多的需求可以參考 nn.ModuleList
或 nn.ModuleDict
,它們跟 Python 的 list 和 dict 基本相似,但是註冊到了 model 里,可以被整个 model 訪問。創建⽅法是传⼊一個 list 或 dict。此外,ModuleDict 內的层不僅可以用 ['name']
訪問(同 Python),還可以用 ModuleDict.name
訪問(同 Module)。
這兩個方法與 nn.Sequential
的區別在於它們不可以單獨作為一個網絡使用,而必須在一個網絡的 init 中初始化,在 forward 中使用。此外,它們擁有 append 和 update 等方法。
初始化參数
如這章開頭例子所示,卷積等的權重和偏置就是一個 torch.Tensor
,將 tensor 傳入初始化函式即可赋上指定的初始化值,一些自帶的初始化函式可在 torch.nn.init 找到,可以看到這些函式都以下劃線結尾,說明它們都是原位改变值的。
一般而言,會寫一個函式來進行一些判斷和循環完成對所有 tensor 的初始化,例如判斷是卷積的 weight
就使用 kaiming_uniform_()
,判斷是卷積的 bias
就使用 zeros_()
。關於如何返回所有的 tensor 可參考下一章。
nn.Module 類的一些属性
自定義的類繼承 nn.Module
之後,就可以調用其自帶方法,下面是一些常用的。為方便說明,這裡以 torchvision 內的 VGG-16 為例。導入後使用 vgg = torchvision.models.vgg16()
即可,具體網絡結構可以打開前面的超鏈接或是 IDE 裡打開。
model.modules()
這個方法會返回一個 generator,裡面是順次逐級的 model 的所有子模塊,如果某一個子模塊是一個 nn.Module
,那麼還返回這個子模塊的所有子模塊。所以返回中第一個值會是整個網絡,第二個值是整個 self.features
,接下來第三個開始會展開,依次返回 self.features
裡的所有层,再接下來是 self.avgpool
,等等。也就是有
1 | list(vgg.modules()) all_modules = |
這個方法主要用於需要判斷網絡层的類型(卷積、全連接)然後進行下一步操作的,判斷一般使用 for m in model.modules(): -> if isinstance(m, nn.Conv2d): -> ...
。例如在初始化 tensor 的值的時候就可以使用,例如 VGG 的 _initialize_weights
對不同類型的层使用不同的初始化方法。
有一個類似的方法 named_modules()
,會返回帶上子模塊名字的字典。還有 children()
和 named_children()
,這兩個只返回直接的一級子模塊,而不會展開它們(如果是 nn.Module
),可以循環調用直到末尾。
model.parameters()
上一個方法的最小單位是 Conv、MaxPool 之類的網絡层,需要通過 m.weight
與 m.bias
訪問其中的權重 tensor,而這個方法會直接返回 model 中所有 tensor,不帶层級結構,每一個元素都是 tensor 且 .requires_grad == True
,因此通常被傳給 optimizer。注意返回的都是和網絡中有相同內存地址的 tensor,如果在外部改变了這些 tensor 的值(正常情況下是被 optimizer),那麼網絡中的參数也將改变。
1 | all_parameters = list(vgg.parameters()) |
同樣的,這個方法也有一個對應的 model.named_paramters()
。
model.state_dict()
與 .named_parameters()
類似,也是直接返回所有權重,但是除了可學習的 tensor 之外,還會返回一些层的狀態(persistent buffers),比如 BatchNorm 层會不斷紀錄 running averages,雖然不是作用在輸入上的權重,但是也是關係到訓練過程。因此這個方法主要用於保存 checkpoint 的時候返回所有需要保存的值。
数據集相關的 Dataset 和 Dataloader
torch.utils.data
下有兩個類,一個是 Dataset
,用來給出一個索引返回一對圖片和標籤;另一個是 Dataloader
,用於將前者以某種方式採樣,並返回一個 batch。
對於 Dataset
,繼承之後重寫 __len__()
和 __getitem()__
。
對於 Dataloader
,有 sampler
和 collate_fn
。
Registry
open-mmlab 和 Facebook 在他們的 MMCV 和 fvcore 中都實現了一個 class Registry
。他們在內部保存一個 dict,創建實例時提供一個 name MODELS = Registry('models')
,然後使用裝飾器 @MODELS.register_module() class ResNet:
或直接調用函數 MODELS.register_module(ResNet)
註冊一個函數到 dict 中。
關於 Python 的裝飾器:裝飾器是一個函數,參數中接收一個函數 A(的地址)並最終返回一個函數 B,在這個返回的函數 B 的 return 中 A 被執行,B 的 return 之前的部分為裝飾器新添加的功能。
在執行到 @decorator
的時候,裝飾器函數的最外層(scope x)就已經執行
1 | def log(func): |
import 時就從頭至尾掃描(執行)了整個文件