Mxnet基础知识(二)
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]
4. Mxnet 常用API
1. mx.nd.pick()
作用:根据指定索引,检索出ndarray中的数据 参数: pick(data=None, index=None, axis=_Null, keepdims=_Null, mode=_Null, out=None, name=None, **kwargs) data:输入数据,ndarray类型 index: 索引,ndarray类型 axis:维度 keepdims: True时表示输出ndarray保持输入ndarray的维度不变 mode: clip表示截断模式,索引超过输入ndarray的尺寸时,截断未最大的尺寸;wrap表示循环模式,索引超过输入ndarray的尺寸时,从开始出循环处理
import mxnet as mx x = mx.nd.array([[1., 2.], [3., 4.], [5., 6.]]) print(mx.nd.pick(x, index=mx.nd.array([0, 1]), axis=0)) #[ 1., 4.] print(mx.nd.pick(x, index=mx.nd.array([0, 1, 0]), axis=1)) #[ 1., 4., 5.] print(mx.nd.pick(x, index=mx.nd.array([0, 1, 0]), axis=1, keepdims=True)) #[[1.],[4.],[5.]] print(mx.nd.pick(x, index=mx.nd.array([2, -1, -2]), axis=1, mode='wrap')) #[ 1., 4., 5.] print(mx.nd.pick(x, index=mx.nd.array([[1.],[0.],[2.]]), axis=1, keepdims=True)) #[[ 2.],[ 3.],[ 6.]]
2. mx.nd.mean() (类似的还有mx.nd.sum(), mx.nd.min(), mx.nd.max())
作用:求平均值 参数: mean(data=None, axis=_Null, keepdims=_Null, exclude=_Null, out=None, name=None, **kwargs): data: 输入ndarray axis:平均的维度,默认情况下会计算ndarray所有值的平均值,返回一个标量 为int时,如axis=1,只计算1这个维度上的平均值 为tuple时,如axis=(1, 2), 计算1和2这两个维度上的平均值 keepdims:输出数据保持输入数据的维度不变 exclude: 默认为False, 设为True,则计算平均值时,不是axis设置的维度会被计算,如:总共有三个维度(0, 1, 2), axis=0, exclude=True时,会计算1和2这两个维度上的平均值
import mxnet as mx x = mx.nd.array([[[1., 2.],[1, 2]], [[3., 4.], [3., 4.]], [[5., 6.], [5., 6.]]]) # 3*3*2 print(mx.nd.mean(x, axis=0)) print(mx.nd.mean(x, axis=1)) print(mx.nd.mean(x, axis=2)) print(mx.nd.mean(x, axis=0, exclude=True)) 输出信息如下: [[3. 4.] [3. 4.]] <NDArray 2x2 @cpu(0)> [[1. 2.] [3. 4.] [5. 6.]] <NDArray 3x2 @cpu(0)> [[1.5 1.5] [3.5 3.5] [5.5 5.5]] <NDArray 3x2 @cpu(0)> [1.5 3.5 5.5] <NDArray 3 @cpu(0)>
3. mx.nd.contrib.box_nms()
作用:对于多个box进行nms(非最大值抑制),目标检测时一般要对模型预测的大量检测框进行nms 参数: box_nms(data=None, overlap_thresh=_Null, valid_thresh=_Null, topk=_Null, coord_start=_Null, score_index=_Null, id_index=_Null, background_id=_Null, force_suppress=_Null, in_format=_Null, out_format=_Null, out=None, name=None, **kwargs) data: 输入的Ndarray,包括预测类别,分数和box信息。典型输入ndarray尺寸信息为b*n*k, b为batch(b也可以不设置), n为多少个box, k一般为6,表示box格式为[id, score, xmin, ymin, xmax, ymax] overlap_thresh: IOU阈值,默认值为0.5;两个box的重叠面积比例超过阈值时会舍弃掉分数低的 valid_thresh:分数阈值, 默认值为0;box的score超过阈值时才进行nms处理 topk:默认值-1,只对分数排序前topk的box进行nms,默认对所有box进行nms coord_start:默认为2, box坐标开始的index ([id, score, xmin, ymin, xmax, ymax]中xmin的index为2) id_index:默认-1, id的index,-1表示忽略id数据 background_id:默认-1, 背景的类别id,-1表示忽略。(若设为0,则进行nms时,id=0的box不进行nms) force_suppress:默认0, 当force_suppress=0, id_index!=-1时,只会对id属于同一类别的box进行nms in_format: 默认为'corner';'corner'表示输入数据box是[id, score, xmin, ymin, xmax, ymax]; 'center'表示输入数据box是[id, score, x_center, y_center, width, height] out_format: 输出数据box的格式, 默认'corner'
ids = mx.nd.array([[0], [1], [0], [2]]) #4x1 scores = mx.nd.array([[0.5], [0.4], [0.3], [0.6]]) #4x1 bboxs = mx.nd.array([[10., 10., 20., 20], [10., 10., 20., 20], [10., 10., 14., 14], [50., 50., 70., 80.]]) #4x4 x = mx.nd.concat(ids, scores, bboxs, dim=-1) #4*6 print(x) result1 = mx.nd.contrib.box_nms(x, overlap_thresh=0.1, coord_start=2, score_index=1, id_index=-1, force_suppress=True, in_format='corner', out_format='corner') print(result1) result2 = mx.nd.contrib.box_nms(x, overlap_thresh=0.1, coord_start=2, score_index=1, id_index=0, force_suppress=False, in_format='corner', out_format='corner') print(result2) 输出结果: [[ 0. 0.5 10. 10. 20. 20. ] [ 1. 0.4 10. 10. 20. 20. ] [ 0. 0.3 10. 10. 14. 14. ] [ 2. 0.6 50. 50. 70. 80. ]] <NDArray 4x6 @cpu(0)> [[ 2. 0.6 50. 50. 70. 80. ] [ 0. 0.5 10. 10. 20. 20. ] [-1. -1. -1. -1. -1. -1. ] [-1. -1. -1. -1. -1. -1. ]] <NDArray 4x6 @cpu(0)> [[ 2. 0.6 50. 50. 70. 80. ] [ 0. 0.5 10. 10. 20. 20. ] [ 1. 0.4 10. 10. 20. 20. ] [-1. -1. -1. -1. -1. -1. ]] <NDArray 4x6 @cpu(0)>