首页 > 技术文章 > Mxnet基础知识(二)

silence-cho 2020-07-13 20:28 原文

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)>

推荐阅读