1 混合式編程
深度學習框架中,pytorch采用命令式編程,tensorflow采用符號式編程。mxnet的gluon則嘗試將命令式編程和符號式編程結合。
1.1 符號式編程和命令式編程
符號式編程更加靈活,便於理解和調試;命令式編程能對代碼進行優化,執行起來效率更高,如下所示:
命令式編程:代碼會根據執行順序,逐行執行
#命令式編程 def add(a, b): return a + b def fancy_func(a, b, c, d): e = add(a, b) f = add(c, d) g = add(e, f) return g fancy_func(1, 2, 3, 4)
符號式編程:下面代碼會通過字符串的形式傳給compile,compile能看到所有的代碼,能對代碼結構和內存進行優化,加快代碼執行效率
#符號式編程 def add_str(): return ''' def add(a, b): return a + b ''' def fancy_func_str(): return ''' def fancy_func(a, b, c, d): e = add(a, b) f = add(c, d) g = add(e, f) return g ''' def evoke_str(): return add_str() + fancy_func_str() + ''' print(fancy_func(1, 2, 3, 4)) ''' prog = evoke_str() print(prog) y = compile(prog, '', 'exec') exec(y)
mxnet構建網絡時除了nn.Block和nn.Sequential外,還有nn.HybridBlock和nn.HybridSequential, 實現在構建時通過命令式編程方式,代碼執行時轉變成符號式編程。HybridBlock和HybridSequential構建的網絡net,通過net.hybride()可以將網絡轉變成符號網絡圖(symbolic graph),對代碼結構進行優化,而且mxnet會緩存符號圖,隨后的前向傳遞中重復使用符號圖。
#coding:utf-8 from mxnet.gluon import nn from mxnet import nd class HybridNet(nn.HybridBlock): def __init__(self, **kwargs): super(HybridNet, self).__init__(**kwargs) self.hidden = nn.Dense(10) self.output = nn.Dense(2) def hybrid_forward(self, F, x): print('F: ', F) print('x: ', x) x = F.relu(self.hidden(x)) print('hidden: ', x) return self.output(x) #按原始命令式編程方程,逐行執行 net = HybridNet() net.initialize() x = nd.random.normal(shape=(1, 4)) net(x) #net.hybridize()會對代碼結構進行優化,轉變成符號式編程 net.hybridize() net(x) #再次執行時,不會打印代碼中的print部分,這是因為hybride后,構建成符號式代碼網絡,mxnet會緩存符號圖,直接執行符號圖,不會再去調用python原始代碼 net(x)
另外,繼承自HybridBlock的網絡需要實現的是hybrid_forward()相比於forward()多了一個參數F,F會根據輸入的x類型選擇執行,即x若為mxnet.ndarry,則F調用ndarry的方法;若x若為mxnet.symbol,則調用symbol的方法。
2. 延遲初始化
在構建網絡時,mxnet支持不指明參數的輸入尺寸,只需指明參數的輸出尺寸。這是通過延遲初始化實現
from mxnet import init, nd from mxnet.gluon import nn def getnet(): net = nn.Sequential() net.add(nn.Dense(256, activation='relu')) net.add(nn.Dense(10)) return net #網絡參數未初始化,無具體值 net = getnet() print(1, net.collect_params()) #print(1, net[0].weight.data()) #網絡參數未初始化,無具體值 net.initialize() print(2, net.collect_params()) #print(2, net[0].weight.data()) #根據輸入x的尺寸,網絡推斷出各層參數的尺寸,然后進行初始化 x = nd.random.uniform(shape=(2, 30)) net(x) print(3, net.collect_params()) print(3, net[0].weight.data())
#第二次執行時,不會再進行初始化
net(x)
init提供了許多初始化方法,如下:
init.Zero() #初始化為常數0 init.One() #初始化為常數1 init.Constant(value=0.05) #初始化為常數0.05 init.Orthogonal() #初始化為正交矩陣 init.Uniform(scale=0.07) #(-0.07, 0.07)之間的隨機分布 init.Normal(sigma=0.01) #均值為0, 標准差為0.01的正態分布 init.Xavier(magnitude=3) # magnitude初始化, 適合tanh init.MSRAPrelu(slope=0.25) #凱明初始化,適合relu
自定義初始化:
#第一層和第二層采用不同的方法進行初始化, # force_reinit:無論網絡是否初始化,都重新初始化 net[0].weight.initialize(init=init.Xavier(), force_reinit=True) net[1].initialize(init=init.Constant(42), force_reinit=True)
#自定義初始化,需要繼承init.Initializer, 並實現 _init_weight 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 # 絕對值小於5的賦值為0, 大於等於5的保持不變 net.initialize(MyInit(), force_reinit=True) net[0].weight.data()[0]
3. 參數和模塊命名
mxnet網絡中的parameter和block都有命名(prefix), parameter的名字由用戶指定,block的名字由用戶或mxnet自動創建
mydense = nn.Dense(100, prefix="mydense_") print(mydense.prefix) #mydense_ print(mydense.collect_params()) #mydense_weight, mydense_bias dense0 = nn.Dense(100) print(dense0.prefix) #dense0_ print(dense0.collect_params()) #dense0_weight, dense0_bias dense1 = nn.Dense(100) print(dense1.prefix) #dense1_ print(dense1.collect_params()) #dense1_weight, dense1_bias
每一個block都有一個name_scope(), 在其上下文中創建的子block,會采用其名字作為前綴, 注意下面model0和model1的名字差別
from mxnet import gluon import mxnet as mx class Model(gluon.Block): def __init__(self, **kwargs): super(Model, self).__init__(**kwargs) with self.name_scope(): self.dense0 = gluon.nn.Dense(20) self.dense1 = gluon.nn.Dense(20) self.mydense = gluon.nn.Dense(20, prefix='mydense_') def forward(self, x): x = mx.nd.relu(self.dense0(x)) x = mx.nd.relu(self.dense1(x)) return mx.nd.relu(self.mydense(x)) model0 = Model() model0.initialize() model0(mx.nd.zeros((1, 20))) print(model0.prefix) #model0_ print(model0.dense0.prefix) #model0_dense0_ print(model0.dense1.prefix) #model0_dense1_ print(model0.mydense.prefix) #model0_mydense_ model1 = Model() model1.initialize() model1(mx.nd.zeros((1, 20))) print(model1.prefix) #model1_ print(model1.dense0.prefix) #model1_dense0_ print(model1.dense1.prefix) #model1_dense1_ print(model1.mydense.prefix) #model1_mydense_
不同的命名,其保存的參數名字也會有差別,在保存和加載模型參數時會引起錯誤,如下所示:
#如下方式保存和加載:model0保存的參數,model1加載會報錯 model0.collect_params().save('model.params') try: model1.collect_params().load('model.params', mx.cpu()) except Exception as e: print(e) print(model0.collect_params(), '\n') print(model1.collect_params()) #如下方式保存和加載:model0保存的參數,model1加載不會報錯 model0.save_parameters('model.params') model1.load_parameters('model.params') print(mx.nd.load('model.params').keys())
在加載預訓練的模型,進行finetune時,注意命名空間, 如下所示:
#加載預訓練模型,最后一層為1000類別的分類器 alexnet = gluon.model_zoo.vision.alexnet(pretrained=True) print(alexnet.output) print(alexnet.output.prefix) #修改最后一層結構為 100類別的分類器,進行finetune with alexnet.name_scope(): alexnet.output = gluon.nn.Dense(100) alexnet.output.initialize() print(alexnet.output)
Sequential創建的net獲取參數:
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() # Use the default initialization method x = nd.random.uniform(shape=(2, 20)) net(x) # Forward computation print(net[0].params) print(net[1].params) #通過屬性獲取 print(net[1].bias) print(net[1].bias.data()) print(net[0].weight.grad()) #通過字典方式獲取 print(net[0].params['dense0_weight']) print(net[0].params['dense0_weight'].data()) #獲取所有參數 print(net.collect_params()) print(net[0].collect_params()) net.collect_params()['dense1_bias'].data() #正則匹配 print(net.collect_params('.*weight')) print(net.collect_params('dense0.*'))
Block創建網絡獲取參數:
from mxnet import gluon import mxnet as mx class Model(gluon.Block): def __init__(self, **kwargs): super(Model, self).__init__(**kwargs) with self.name_scope(): self.dense0 = gluon.nn.Dense(20) self.dense1 = gluon.nn.Dense(20) self.mydense = gluon.nn.Dense(20, prefix='mydense_') def forward(self, x): x = mx.nd.relu(self.dense0(x)) x = mx.nd.relu(self.dense1(x)) return mx.nd.relu(self.mydense(x)) model0 = Model() model0.initialize() model0(mx.nd.zeros((1, 20))) #通過有序字典_children print(model0._children) print(model0._children['dense0'].weight._data) print(model0._children['dense0'].bias._data) #通過收集所有參數 print(model0.collect_params()['model0_dense0_weight']._data) print(model0.collect_params()['model0_dense0_bias']._data)
Parameter和ParameterDict
gluon.Parameter類能夠創建網絡中的參數,gluon.ParameterDict類是字典,建立了parameter name和parameter實例之間的映射,通過ParameterDict也可以創建parameter.
Parameter的使用
class MyDense(nn.Block): def __init__(self, units, in_units, **kwargs): # units: the number of outputs in this layer # in_units: the number of inputs in this layer super(MyDense, self).__init__(**kwargs) self.weight = gluon.Parameter('weight', shape=(in_units, units)) #創建名為weight的參數 self.bias = gluon.Parameter('bias', shape=(units,)) #創建名為bias的參數 def forward(self, x): linear = nd.dot(x, self.weight.data()) + self.bias.data() return nd.relu(linear)
net = nn.Sequential() net.add(MyDense(units=8, in_units=64), MyDense(units=1, in_units=8)) #初始化參數 for block in net: if hasattr(block, "weight"): block.weight.initialize() if hasattr(block, "bias"): block.bias.initialize() print(net(nd.random.uniform(shape=(2, 64)))) print(net)
ParameterDict使用
#創建一個parameterdict,包含一個名為param2的parameter
params = gluon.ParameterDict() params.get('param2', shape=(2, 3)) print(params) print(params.keys()) print(params['param2'])
自定義初始化方法
有時候我們需要的初始化方法並沒有在init
模塊中提供。這時,可以實現一個Initializer
類的子類,從而能夠像使用其他初始化方法那樣使用它。通常,我們只需要實現_init_weight
這個函數,並將其傳入的NDArray
修改成初始化的結果。在下面的例子里,我們令權重有一半概率初始化為0,有另一半概率初始化為[-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]