參數管理
一旦選擇了架構并設定了超參數,就進入了訓練階段。此時,目标是找到使損失函數最小化的參數值。經過訓練後需要使用這些參數來做出未來的預測。此外,有時我們希望提取參數,以便在其他環境中複用它們,将模型儲存到磁盤,以便它可以在其他軟體中執行,或者為了獲得科學的了解而進行檢查。大多數情況下,可以忽略聲明和操作參數的具體細節,而隻依靠深度學習架構來完成繁重的工作。然而,當我們離開具有标準層的層疊架構時,我們有時會陷入聲明和操作參數的麻煩中。在本節中,我們将介紹以下内容:
通路參數,用于調試、診斷和可視化。
參數初始化。
在不同模型元件間共享參數。
我們首先關注具有單隐藏層的多層感覺機。
import torch
from torch import nn
net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(), nn.Linear(8, 1))
X = torch.rand(size=(2, 4))
net(X)
1 參數通路
我們從已有模型中通路參數。當通過Sequential類定義模型時,我們可以通過索引來通路模型的任意層。這就像模型是一個清單一樣。每層的參數都在其屬性中。如下所示,我們可以檢查第二個全連接配接層的參數。
print(net[2].state_dict())
OrderedDict([('weight', tensor([[-0.0578, 0.2847, 0.0501, -0.1246, 0.2490, -0.0303, 0.1356, 0.2373]])), ('bias', tensor([0.1629]))])
輸出的結果告訴我們一些重要的事情。首先,這個全連接配接層包含兩個參數,分别是該層的權重和偏置。兩者都存儲為單精度浮點數(float32)。注意,參數名稱允許我們唯一地辨別每個參數,即使在包含數百個層的網絡中也是如此。
(1) 目标參數
注意,每個參數都表示為參數(parameter)類的一個執行個體。要對參數執行任何操作,首先我們需要通路底層的數值。有幾種方法可以做到這一點。有些比較簡單,而另一些則比較通用。下面的代碼從第二個神經網絡層提取偏置,提取後傳回的是一個參數類執行個體,并進一步通路該參數的值。。
print(type(net[2].bias))
print(net[2].bias)
print(net[2].bias.data)
<class 'torch.nn.parameter.Parameter'>
Parameter containing:
tensor([0.1629], requires_grad=True)
tensor([0.1629])
參數是複合的對象,包含值、梯度和額外資訊。這就是我們需要顯式請求值的原因。除了值之外,我們還可以通路每個參數的梯度。由于我們還沒有調用這個網絡的反向傳播,是以參數的梯度處于初始狀态。
net[2].weight.grad == None
True
(2) 一次性通路所有參數
當我們需要對所有參數執行操作時,逐個通路它們可能會很麻煩。當我們處理更複雜的塊(例如,嵌套塊)時,情況可能會變得特别複雜,因為我們需要遞歸整個樹來提取每個子塊的參數。下面,我們将通過示範來比較通路第一個全連接配接層的參數和通路所有層。
*為解包操作
print(*[(name, param.shape) for name, param in net[0].named_parameters()])
print(*[(name, param.shape) for name, param in net.named_parameters()])
('weight', torch.Size([8, 4])) ('bias', torch.Size([8]))
('0.weight', torch.Size([8, 4])) ('0.bias', torch.Size([8])) ('2.weight', torch.Size([1, 8])) ('2.bias', torch.Size([1]))
這為我們提供了另一種通路網絡參數的方式,如下所示
net.state_dict()['2.bias'].data
tensor([0.1629])
(3) 從嵌套塊收集參數
如果我們将多個塊互相嵌套,參數命名約定是如何工作的。為此,我們首先定義一個生成塊的函數(可以說是塊工廠),然後将這些塊組合到更大的塊中。
def block1():
return nn.Sequential(nn.Linear(4, 8), nn.ReLU(),
nn.Linear(8, 4), nn.ReLU())
def block2():
net = nn.Sequential()
for i in range(4):
# 在這裡嵌套
net.add_module(f'block {i}', block1())
return net
rgnet = nn.Sequential(block2(), nn.Linear(4, 1))
rgnet(X)
tensor([[-0.0842],
[-0.0842]], grad_fn=<AddmmBackward>)
看看它是如何組織的
print(rgnet)
Sequential(
(0): Sequential(
(block 0): Sequential(
(0): Linear(in_features=4, out_features=8, bias=True)
(1): ReLU()
(2): Linear(in_features=8, out_features=4, bias=True)
(3): ReLU()
)
(block 1): Sequential(
(0): Linear(in_features=4, out_features=8, bias=True)
(1): ReLU()
(2): Linear(in_features=8, out_features=4, bias=True)
(3): ReLU()
)
(block 2): Sequential(
(0): Linear(in_features=4, out_features=8, bias=True)
(1): ReLU()
(2): Linear(in_features=8, out_features=4, bias=True)
(3): ReLU()
)
(block 3): Sequential(
(0): Linear(in_features=4, out_features=8, bias=True)
(1): ReLU()
(2): Linear(in_features=8, out_features=4, bias=True)
(3): ReLU()
)
)
(1): Linear(in_features=4, out_features=1, bias=True)
)
因為層是分層嵌套的,是以我們也可以像通過嵌套清單索引一樣通路它們。例如,我們下面通路第一個主要的塊,其中第二個子塊的第一層的偏置項。
rgnet[0][1][0].bias.data
tensor([-0.2009, 0.4356, -0.0054, 0.4656, 0.1657, 0.0178, 0.3627, 0.0615])
2 參數初始化
我們知道了如何通路參數,現在讓我們看看如何正确地初始化參數。了良好初始化很有必要。深度學習架構提供預設随機初始化。然而,我們經常希望根據其他規則初始化權重。深度學習架構提供了最常用的規則,也允許建立自定義初始化方法。
預設情況下,PyTorch會根據一個範圍均勻地初始化權重和偏置矩陣,這個範圍是根據輸入和輸出次元計算出的。PyTorch的nn.init子產品提供了多種預置初始化方法。
(1) 内置初始化
讓我們首先調用内置的初始化器。下面的代碼将所有權重參數初始化為标準差為0.01的高斯随機變量,且将偏置參數設定為0。
def init_normal(m):
if type(m) == nn.Linear:
nn.init.normal_(m.weight, mean=0, std=0.01)#函數名後面跟一個下劃線表示替換函數不是傳回值的函數
nn.init.zeros_(m.bias)
net.apply(init_normal)#net.apply()這個函數表示周遊一個函數符合要求的進行相應的操作
net[0].weight.data[0], net[0].bias.data[0]
(tensor([-0.0004, 0.0054, 0.0049, 0.0013]), tensor(0.))
我們還可以将所有參數初始化為給定的常數(比如1)。
def init_constant(m):
if type(m) == nn.Linear:
nn.init.constant_(m.weight, 1)
nn.init.zeros_(m.bias)
net.apply(init_constant)
net[0].weight.data[0], net[0].bias.data[0]
(tensor([1., 1., 1., 1.]), tensor(0.))
我們還可以對某些塊應用不同的初始化方法。例如,下面我們使用Xavier初始化方法初始化第一層,然後第二層初始化為常量值
def xavier(m):
if type(m) == nn.Linear:
nn.init.xavier_uniform_(m.weight)
def init_42(m):
if type(m) == nn.Linear:
nn.init.constant_(m.weight, 42)
net[0].apply(xavier)
net[2].apply(init_42)
print(net[0].weight.data[0])
print(net[2].weight.data)
tensor([-0.1367, -0.2249, 0.4909, -0.6461])
tensor([[42., 42., 42., 42., 42., 42., 42., 42.]])
(2) 内置初始化
有時,深度學習架構沒有提供我們需要的初始化方法。在下面的例子中,我們使用以下的分布為任意權重參數 w 定義初始化方法:

同樣,我們實作了一個my_init函數來應用到net。
def my_init(m):
if type(m) == nn.Linear:
print("Init", *[(name, param.shape)
for name, param in m.named_parameters()][0])
nn.init.uniform_(m.weight, -10, 10)
m.weight.data *= m.weight.data.abs() >= 5
net.apply(my_init)
net[0].weight[:2]
注意,我們始終可以直接設定參數。
net[0].weight.data[:] += 1
net[0].weight.data[0, 0] = 42
net[0].weight.data[0]
3 參數綁定
有時我們希望在多個層間共享參數。讓我們看看如何優雅地做這件事。在下面,我們定義一個稠密層,然後使用它的參數來設定另一個層的參數。
# 我們需要給共享層一個名稱,以便可以引用它的參數。
shared = nn.Linear(8, 8)
net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(),
shared, nn.ReLU(),
shared, nn.ReLU(),
nn.Linear(8, 1))
net(X)
# 檢查參數是否相同
print(net[2].weight.data[0] == net[4].weight.data[0])
net[2].weight.data[0, 0] = 100
# 確定它們實際上是同一個對象,而不隻是有相同的值。
print(net[2].weight.data[0] == net[4].weight.data[0])
這個例子表明第二層和第三層的參數是綁定的。它們不僅值相等,而且由相同的張量表示。是以,如果我們改變其中一個參數,另一個參數也會改變。你可能會想,當參數綁定時,梯度會發生什麼情況?答案是由于模型參數包含梯度,是以在反向傳播期間第二個隐藏層和第三個隐藏層的梯度會加在一起。
總結
我們有幾種方法可以通路、初始化和綁定模型參數。
我們可以使用自定義初始化方法。
共享參數通常可以節省記憶體,并在以下方面具有特定的好處:
對于圖像識别中的CNN,共享參數使網絡能夠在圖像中的任何地方而不是僅在某個區域中查找給定的功能。
對于RNN,它在序列的各個時間步之間共享參數,是以可以很好地推廣到不同序列長度的示例。
對于自動編碼器,編碼器和解碼器共享參數。 在具有線性激活的單層自動編碼器中,共享權重會在權重矩陣的不同隐藏層之間強制正交。