4.2 Module容器——Containers
容器的概念出現在日常生活的方方面面,每天喝水的杯子用來裝一些水,書包用來裝一些辦公用品,衣櫃用來裝一些衣服。因此,我們可以很容易地抽象出容器的概念,它用於將一些物品放置在某個地方,並進行有效的管理和使用。
在深度學習模型裡面,有一些網路層需要放在一起使用,如 conv + bn + relu 的組合。Module的容器是將一組操作捆綁在一起的工具,在pytorch官方文檔中把Module也定義為Containers,或許是因為“Modules can also contain other Modules”。
對於Module類可查看4.1小結,這裡詳細介紹兩個常用的容器Sequential與ModuleList,同時介紹ModuleDict,ParameterList,ParameterDict。
Sequential
sequential是pytorch裡使用最廣泛的容器,它的作用是將一系列網路層按固定的先後順序串起來,當成一個整體,調用時資料從第一個層按循序執行到最後一個層。回顧一下transforms的Compose就可以體會到按順序的含義了。
sequential可以直接傳module,也可以傳OrderedDict,OrderedDict可以讓容器裡的每個module都有名字,方便調用。
請看兩段官方代碼:
model = nn.Sequential(
nn.Conv2d(
1,
20,
5),
nn.ReLU(),
nn.Conv2d(
20,
64,
5),
nn.ReLU()
)
# Using Sequential with OrderedDict. This is functionally the
# same as the above code
model = nn.Sequential(OrderedDict([
(
'conv1', nn.Conv2d(
1,
20,
5)),
(
'relu1', nn.ReLU()),
(
'conv2', nn.Conv2d(
20,
64,
5)),
(
'relu2', nn.ReLU())
]))
來看一個實際案例:
AlexNet是新一代CNN的開山之作,也是這一輪深度學習潮流裡,電腦視覺任務的開山之作。對於現代CNN,通常會把前面的卷積層、池化層稱為特徵提取部分,最後的全連接層當作分類器,這一點在代碼編寫上將有所體現。
例如,在D:\Anaconda_data\envs\pytorch_1.10_gpu\Lib\site-packages\torchvision\models\alexnet.py 下的AlexNet,它把前面的卷積和池化都放到Sequential這個容器當中,並且命名為self.features。在最後還有一個名為self.classifier的Sequential容器,它包含3個Linear層及啟動函數、Dropout。這裡正如下圖所示,把模型大卸兩塊。
def forward(self, x: torch.Tensor) -> torch.Tensor:
x = self.features(x)
x = self.avgpool(x)
x = torch.flatten(x,
1)
x = self.classifier(x)
return
x
Sequential的調用
Sequential容器把一堆module包起來之後,它在forward中是如何使用的呢? 下麵請看配套代碼,主要採用debug模式,觀察Alexnet的forward中,兩個Sequential容器是如何forward的,同時也要看看Alexnet這個模型的屬性。
- 在 output = model(fake_input) 設置中斷點,step into,然後進入熟悉的_call_impl,關於module內部的代碼這裡直接略過,不熟悉的朋友請回到4.1小節閱讀。
- 這裡直接跳到Alexnet類的forward函數,第一行就是執行x = self.features(x),繼續step into觀察這個sequential容器是如何工作的,進入它
- 來到了Module類下的_call_impl函數:沒錯,又來到了_call_impl,因為sequential它也是一個module,因此在調用self.features的時候,會進入_call_impl, 下面需要大家有耐心的進入self.features的forward函數,其實就是1102行進去;
- 來到Sequential類的forward函數,它十分簡潔,如下所示:
def forward(self, input):
formodule
inself:
input = module(input)
returninput
這段代碼是不是十分的熟悉呢? transforms當中也是這樣去實現一個Compose裡的變換的。
從此處可知道,Sequential類的功能是將一系列網路層按固定的先後順序串起來,當成一個整體,調用時資料從第一個層按循序執行到最後一個層,各層之間的資料必須能對接起來。
接著回到 Alexnet的forward函數下,觀察一下Alexnet這個module的屬性
重點看_modules屬性,它有3個key-value,其中有2個是Sequential類,因為Sequential屬於module類,繼續展開一個Sequential來看看
可以看到該容器下的一系列網路層,並且是排了序的,這些對於後續理解網路結構、理解網路權重載入的key是十分重要的
ModuleList
ModuleList 是將各個網路層放到一個“列表”中,便於反覆運算的形式調用。
ModuleList與python List的區別
這裡注意是“列表”而不是列表,因為ModuleList管理的module與python的List管理的module是有不同的,大家是否還記得module的setattr函數?在那裡會對類屬性進行判斷管理,只有ModuleList裡的網路層才會被管理,如果是List裡的網路層則不會被管理,也就不能反覆運算更新了。
ModuleList 使用示例
假設要構建一個10層的全連接網路,如果用Sequential,那就要手寫10行nn.Linear,而用ModuleList是這樣的:
class MyModule(nn.Module):
def __init__(self):
super(MyModule, self).__init__()
self.linears = nn.ModuleList([nn.Linear(
10,
10)
fori
inrange(
10)])
# self.linears = [nn.Linear(10, 10) for i in range(10)] # 觀察model._modules,將會是空的
def forward(self, x):
for
sub_layer
inself.linears:
x = sub_layer(x)
return
x
需要對python的 list進行一個ModuleList封裝,這樣才可以在model的_modules屬性下看到創建的10個Linear層。
推薦大家看看class ModuleList(Module)的實現,裡邊並不會像Sequential那樣提供forward,即管理的網路層由使用者自行調用,可以for迴圈全用,也可以通過if判斷,有條件的選擇部分網路層使用。同時ModuleList也提供了類似List的方法,insert\append\extend等。
ModuleDict
ModuleList可以像python的List一樣管理各個module,但對於索引而言有一些不方便,因為它沒有名字,需要記住是第幾個元素才能定位到指定的層,這在深度神經網路中有一點不方便。
而ModuleDict就是可以像python的Dict一樣為每個層賦予名字,可以根據網路層的名字進行選擇性的調用網路層。
請看代碼
class MyModule2(nn.Module):
def __init__(self):
super(MyModule2, self).__init__()
self.choices = nn.ModuleDict({
'conv'
: nn.Conv2d(
3,
16,
5),
'pool'
: nn.MaxPool2d(
3)
})
self.activations = nn.ModuleDict({
'lrelu'
: nn.LeakyReLU(),
'prelu'
: nn.PReLU()
})
def forward(self, x, choice, act):
x = self.choices[choice](x)
x = self.activations[act](x)
return
x
ParameterList & ParameterDict
除了Module有容器,Parameter也有容器。與ModuleList和ModuleDict類似的,Paramter也有List和Dict,使用方法一樣,這裡就不詳細展開,可以參考Module的容器。
可以看兩段官方文檔代碼感受一下
class MyModule(nn.Module):
def __init__(self):
super(MyModule, self).__init__()
self.params = nn.ParameterDict({
'left'
: nn.Parameter(torch.randn(
5,
10)),
'right'
: nn.Parameter(torch.randn(
5,
10))
})
def forward(self, x, choice):
x = self.params[choice].mm(x)
return
x
# ParameterList
class MyModule(nn.Module):
def __init__(self):
super(MyModule, self).__init__()
self.params = nn.ParameterList([nn.Parameter(torch.randn(
10,
10))
fori
inrange(
10)])
def forward(self, x):
# ParameterList can act as an iterable, or be indexed using ints
for
i, p
inenumerate(self.params):
x = self.params[i //
2].mm(x) + p.mm(x)
return
x
小結
隨著深度神經網路拓撲結構越來越複雜,網路模組多,雜,亂,因此需要Module容器來管理、組織各個網路層,便於forward函數中調用。
使用頻率最高的是Sequential,其次是ModuleList,其餘的均為進階用法,在各類魔改網路中才會涉及。
這裡深刻理解Sequential的機制、理解一個module是如何把Sequential裡的module管理到自己的_modules屬性中,對於後續使用模型是非常重要的。
熟悉了Module類,各種容器封裝,下一小節將介紹一些常用的網路層,如卷積、池化、全連接、啟動函數等。