我們將深入講解模型參數的訪問和初始化,以及如何在多個層之間共享同一份參數。
之前我們一直在使用默認的初始函數,net.initialize()。
from mxnet import init, nd
from mxnet.gluon import nn
net = nn.Sequential()
net.add(nn.Dense(256, activation='relu'))
net.add(nn.Dense(10))
net.initialize()
x = nd.random.uniform(shape=(2,20))
y = net(x)
這里我們從 MXNet 中導入了 init 這個包,它包含了多種模型初始化方法。
訪問模型參數
我們知道可以通過 [] 來訪問 Sequential 類構造出來的網絡的特定層。對於帶有模型參數的層,我們可以通過 Block 類的 params 屬性來得到它包含的所有參數。例如我們查看隱藏層的參數:
net[0].params
# output
dense0_ (
Parameter dense0_weight (shape=(256, 20), dtype=float32)
Parameter dense0_bias (shape=(256,), dtype=float32)
)
我們得到了一個由參數名稱映射到參數實例的字典。第一個參數的名稱為 dense0_weight,它由 net[0] 的名稱(dense0_)和自己的變量名(weight)組成。而且可以看到它參數的形狀為 (256, 20),且數據類型為 32 位浮點數。
為了訪問特定參數,我們既可以通過名字來訪問字典里的元素,也可以直接使用它的變量名。下面兩種方法是等價的,但通常后者的代碼可讀性更好。
net[0].params['dense0_weight'], net[0].weight
Gluon 里參數類型為 Parameter 類,其包含參數權重和它對應的梯度,它們可以分別通過 data 和 grad 函數來訪問。因為我們隨機初始化了權重,所以它是一個由隨機數組成的形狀為 (256, 20) 的 NDArray.
net[0].weight.data()
# output
[[ 0.06700657 -0.00369488 0.0418822 ..., -0.05517294 -0.01194733
-0.00369594]
...,
[ 0.00297424 -0.0281784 -0.06881659 ..., -0.04047417 0.00457048
0.05696651]]
<NDArray 256x20 @cpu(0)>
梯度的形狀跟權重一樣。但由於我們還沒有進行反向傳播計算,所以它的值全為 0.
net[0].weight.grad()
# output
[[ 0. 0. 0. ..., 0. 0. 0.]
...,
[ 0. 0. 0. ..., 0. 0. 0.]]
<NDArray 256x20 @cpu(0)>
類似我們可以訪問其他的層的參數。例如輸出層的偏差權重:
net[1].bias.data()
最后,我們可以 collect_params 函數來獲取 net 實例所有嵌套(例如通過 add 函數嵌套)的層所包含的所有參數。它返回的同樣是一個參數名稱到參數實例的字典。
net.collect_params()
# output
sequential0_ (
Parameter dense0_weight (shape=(256, 20), dtype=float32)
Parameter dense0_bias (shape=(256,), dtype=float32)
Parameter dense1_weight (shape=(10, 256), dtype=float32)
Parameter dense1_bias (shape=(10,), dtype=float32)
)
初始化模型參數
當使用默認的模型初始化,Gluon 會將權重參數元素初始化為 [-0.07, 0.07] 之間均勻分布的隨機數,偏差參數則全為 0. 但經常我們需要使用其他的方法來初始話權重,MXNet 的 init 模塊里提供了多種預設的初始化方法。例如下面例子我們將權重參數初始化成均值為 0,標准差為 0.01 的正態分布隨機數。
# 非首次對模型初始化需要指定 force_reinit。
net.initialize(init=init.Normal(sigma=0.01), force_reinit=True)
net[0].weight.data()[0]
如果想只對某個特定參數進行初始化,我們可以調用 Paramter 類的 initialize 函數,它的使用跟 Block 類提供的一致。下例中我們對第一個隱藏層的權重使用 Xavier 初始化方法。
net[0].weight.initialize(init=init.Xavier(), force_reinit=True)
net[0].weight.data()[0]
自定義初始化方法
有時候我們需要的初始化方法並沒有在 init 模塊中提供。這時,我們可以實現一個 Initializer 類的子類使得我們可以跟前面使用 init.Normal 那樣使用它。通常,我們只需要實現 _init_weight 這個函數,將其傳入的 NDArray 修改成需要的內容。下面例子里我們把權重初始化成 [-10,-5] 和 [5,10] 兩個區間里均勻分布的隨機數。
class MyInit(init.Initializer):
def _init_weight(self, name, data):
print('Init', name, data.shape)
data[:] = nd.random.uniform(low=-10, high=10, shape=data.shape)
data *= data.abs() >= 5
net.initialize(MyInit(), force_reinit=True)
net[0].weight.data()[0]
此外,我們還可以通過 Parameter 類的 set_data 函數來直接改寫模型參數。例如下例中我們將隱藏層參數在現有的基礎上加 1。
net[0].weight.set_data(net[0].weight.data() + 1)
net[0].weight.data()[0]
共享模型參數
在有些情況下,我們希望在多個層之間共享模型參數。我們在 “模型構造” 一節看到了如何在 Block 類里 forward 函數里多次調用同一個類來完成。
這里將介紹另外一個方法,它在構造層的時候指定使用特定的參數。如果不同層使用同一份參數,那么它們不管是在前向計算還是反向傳播時都會共享共同的參數。
我們讓模型的第二隱藏層和第三隱藏層共享模型參數。
net = nn.Sequential()
shared = nn.Dense(8, activation='relu')
net.add(nn.Dense(8, activation='relu'),
shared,
nn.Dense(8, activation='relu', params=shared.params),
nn.Dense(10))
net.initialize()
x = nd.random.uniform(shape=(2,20))
net(x)
net[1].weight.data()[0] == net[2].weight.data()[0]
# output
[ 1. 1. 1. 1. 1. 1. 1. 1.]
<NDArray 8 @cpu(0)>
我們在構造第三隱藏層時通過 params 來指定它使用第二隱藏層的參數。由於模型參數里包含了梯度,所以在反向傳播計算時,第二隱藏層和第三隱藏層的梯度都會被累加在 shared.params.grad() 里。
延后的初始
注意到前面使用 Gluon 的章節里,我們在創建全連接層時都沒有指定輸入大小。例如在一直使用的多層感知機例子里,我們創建了輸出大小為 256 的隱藏層。但是當在調用 initialize 函數的時候,我們並不知道這個層的參數到底有多大,因為它的輸入大小仍然是未知。
只有在當我們將形狀是 (2,20) 的 x 輸入進網絡時,我們這時候才知道這一層的參數大小應該是 (256,20)。所以這個時候我們才能真正開始初始化參數。
使用 MyInit 實例來進行初始化:
from mxnet import init, nd
from mxnet.gluon import nn
class MyInit(init.Initializer):
def _init_weight(self, name, data):
print('Init', name, data.shape)
# 實際的初始化邏輯在此省略了。
net = nn.Sequential()
net.add(nn.Dense(256, activation='relu'))
net.add(nn.Dense(10))
net.initialize(init=MyInit())
# 注意到 MyInit 在調用時會打印信息,但當前我們並沒有看到相應的日志。下面我們執行前向計算。
x = nd.random.uniform(shape=(2,20))
y = net(x)
# output
Init dense0_weight (256, 20)
Init dense1_weight (10, 256)
我們將這個系統將真正的參數初始化延后到獲得了足夠信息到時候稱之為延后初始化。它可以讓模型創建更加簡單,因為我們只需要定義每個層的輸出大小,而不用去推測它們的的輸入大小。這個對於之后將介紹的多達數十甚至數百層的網絡尤其有用。
當然延后初始化也可能會造成一定的困解。在調用第一次前向計算之前我們無法直接操作模型參數。例如無法使用 data 和 set_data 函數來獲取和改寫參數。所以經常我們會額外調用一次 net(x) 來是的參數被真正的初始化。
避免延后初始化
當系統在調用 initialize 函數時能夠知道所有參數形狀,那么延后初始化就不會發生。我們這里給兩個這樣的情況。
第一個是模型已經被初始化過,而且我們要對模型進行重新初始化時。因為我們知道參數大小不會變,所以能夠立即進行重新初始化。
net.initialize(init=MyInit(), force_reinit=True)
第二種情況是我們在創建層到時候指定了每個層的輸入大小,使得系統不需要額外的信息來推測參數形狀。下例中我們通過 in_units 來指定每個全連接層的輸入大小,使得初始化能夠立即進行。
net = nn.Sequential()
net.add(nn.Dense(256, in_units=20, activation='relu'))
net.add(nn.Dense(10, in_units=256))
net.initialize(init=MyInit())