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 | all_modules = list(vgg.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 時就從頭至尾掃描(執行)了整個文件