《第四章》

4.1模型构造

# Multilayer perceptrons,多层感知机
class MLP(nn.Block):
    # 声明带有模型参数的层,这里声明了两个全连接层
    def __init__(self, **kwargs):
        # 调用MLP父类Block的构造函数来进行必要的初始化。这样在构造实例时还可以指定其他函数
        # 参数,如“模型参数的访问、初始化和共享”一节将介绍的模型参数params
        super(MLP, self).__init__(**kwargs)
        self.hidden = nn.Dense(256, activation='relu')  # 隐藏层
        self.output = nn.Dense(10)  # 输出层

    # 定义模型的前向计算,即如何根据输入x计算返回所需要的模型输出
    # MLP类中无须定义反向传播函数。系统将通过自动求梯度而自动生成反向传播所需的backward函数。
    def forward(self, x):
        return self.output(self.hidden(x))


X = nd.random.uniform(shape=(2, 20))
net = MLP()
net.initialize()
net(X)
  1. 模型构造-->实例化-->初始化-->前向计算

  2. 系统将通过自动求梯度而自动生成反向传播所需的backward函数。

  3. Sequential类继承自Block类,成员变量_children里,其类型是OrderedDict。当MySequential实例调用initialize函数时,系统会自动对_children里所有成员初始化.

  4. self.params.get_constant创建的随机权重参数不会在训练中被迭代.

  5. 都是block的子类的实例,可以在net.add()中嵌套使用。

练习

  • 如果不在MLP类的__init__函数里调用父类的__init__函数,会出现什么样的错误信息? 答:'NestMLP' object has no attribute '_children';成员变量_children里,其类型是OrderedDict。当MySequential实例调用

  • 如果去掉FancyMLP类里面的asscalar函数,会有什么问题?

    答:会将1和0.8广播成和X相同的向量来比较,如果是只有一个的一维数据还好,其他的话会产生错误判断。

  • 如果将NestMLP类中通过Sequential实例定义的self.net改为self.net = [nn.Dense(64, activation='relu'), nn.Dense(32, activation='relu')],会有什么问题?

    答:定义失败,Changing attribute type for net from to is not allowed.

4.2模型参数的访问、初始化和共享

访问多层感知机net中隐藏层的所有参数。索引0表示隐藏层为Sequential实例最先添加的层。每次新建的dense不会被自动删掉,所以执行多次的话,dense0_会依次递增。

net[0].params, type(net[0].params)
————————————————输出————————————————
(dense0_ (
   Parameter dense0_weight (shape=(256, 20), dtype=float32)
   Parameter dense0_bias (shape=(256,), dtype=float32)
 ), mxnet.gluon.parameter.ParameterDict)
net[0].params['dense0_weight'], net[0].weight # 两者等价通常后者的代码可读性更好。
net[0].weight.data() # 访问权重数据
net[0].weight.grad() # 访问权重梯度
net[0].bias.data() # 访问偏差数据    
net.collect_params() # 获取net()所嵌套的所有数据(权重和偏差)
net.collect_params('.*weight') # 通过正则表达式获取所有权重

MXNet的init模块里提供了多种预设的初始化方法。也可以用自定义的方法初始化

# 非首次对模型初始化需要指定force_reinit为真,默认仅初始化权重,偏差清零
net.initialize(init=init.Normal(sigma=0.01), force_reinit=True)
# 使用Xavier(特殊均值分布)的方法初始化权重
net[0].weight.initialize(init=init.Xavier(), force_reinit=True) #
# 自定义初始化
class MyInit(init.Initializer):
    # 只需要实现_init_weight这个函数,并将其传入的NDArray修改成初始化的结果。
    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)

可以通过设置params来共享初始化参数或自定义。

net.add(nn.Dense(8, activation='relu'),
        shared,
        nn.Dense(8, activation='relu', params=shared.params),
        nn.Dense(10))

练习

  • 查阅有关init模块的MXNet文档,了解不同的参数初始化方法。

    答:https://mxnet.apache.org/api/python/docs/api/initializer/index.html

    有上采样、常量、描述符、混合方法、正态、均值、一值、零值、Xavier(特殊均值)等多种初始化方法。

  • 尝试在net.initialize()后、net(X)前访问模型参数,观察模型参数的形状。

    答:模型参数形状如下。

    net.add(nn.Dense(8, activation='relu'),
            shared,
            nn.Dense(8, activation='relu', params=shared.params),
            nn.Dense(10))
    net[0],net[1],net[2],net[3]
    ————————————————————输出——————————————————————————
    (Dense(None -> 8, Activation(relu)),
     Dense(None -> 8, Activation(relu)),
     Dense(None -> 8, Activation(relu)),
     Dense(None -> 10, linear))
  • 构造一个含共享参数层的多层感知机并训练。在训练过程中,观察每一层的模型参数和梯度。

    答:共享层的weight一致,grad会根据共享的次数累加。(未测试)

4.3延后初始化

当调用initialize时,因为不知道输入参数X的形状,所以后续结点的形状也不知。

只有当net(X)时,才真正初始化。

避免延后初始化的方法有两种:

# 通过重新初始化来避免
net.initialize(init=MyInit(), force_reinit=True)
# 第二种情况是我们在创建层的时候指定了它的输入个数
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())

练习

  • 如果在下一次前向计算net(X)前改变输入X的形状,包括批量大小和输入个数,会发生什么?

    答:改变形状(2,10):Shape inconsistent, Provided = [256,20], inferred shape=(256,10)

    改变输入个数:Shape inconsistent;改变批量大小,不改变输入个数:无影响。

4.4自定义层

# 它使用ReLU函数作为激活函数。其中in_units和units分别代表输入个数和输出个数。
class MyDense(nn.Block):
    # units为该层的输出个数,in_units为该层的输入个数
    def __init__(self, units, in_units, **kwargs):
        super(MyDense, self).__init__(**kwargs)
        self.weight = self.params.get('weight', shape=(in_units, units))
        self.bias = self.params.get('bias', shape=(units,))

    def forward(self, x):
        linear = nd.dot(x, self.weight.data()) + self.bias.data()
        return nd.relu(linear)

dense = MyDense(units=3, in_units=5)
dense.initialize()
dense(nd.random.uniform(shape=(2, 5))) # 直接使用自定义层做前向计算。
# 嵌套使用
net = nn.Sequential()
net.add(MyDense(8, in_units=64),
        MyDense(1, in_units=8))
net.initialize()
net(nd.random.uniform(shape=(2, 64)))

练习

  • 自定义一个层,使用它做一次前向计算。

    class MyDense(nn.Block):
        # units为该层的输出个数,in_units为该层的输入个数
        def __init__(self, units, in_units, **kwargs):
            super(MyDense, self).__init__(**kwargs)
            self.weight = self.params.get('weight', shape=(in_units, units))
            self.bias = self.params.get('bias', shape=(units,))
    
        def forward(self, x):
            linear = nd.dot(x, self.weight.data()) + self.bias.data()
            return nd.relu(linear)
    
    dense = MyDense(units=3, in_units=5)
    dense.initialize()
    dense(nd.random.uniform(shape=(1000, 5)))

4.5读取与存储

nd.save('filename',params) # 存储为文件
params = nd.load('filename') # 从文件读取
net.save_parameters(filename) # 存储参数文件
net2.load_parameters(filename) # 读取参数文件

练习

  • 即使无须把训练好的模型部署到不同的设备,存储模型参数在实际中还有哪些好处?

    答:1.通过外存来存储,减少内存的压力。2.方便其他训练模型的复用。3.方便导入到其他地方。

4.6GPU计算

  1. 所有nd有关的变量的初始化,记得加上ctx=mx.gpu()

  2. net.initialize()时,记得加上ctx=mx.gpu()

  3. y = x.copyto(mx.gpu()) # 拷贝到GPU,耗内存
    z = x.as_in_context(mx.gpu()) # 转换到GPU
  4. cpu和gpu上的变量不能同时运算,不同gpu上的也不能运算,因为I/O耗费时间。

  5. GPU的并行运算优于CPU

练习

  • 试试大一点儿的计算任务,如大矩阵的乘法,看看使用CPU和GPU的速度区别。如果是计算量很小的任务呢?

    答:大矩阵乘法gpu比cpu快,两个(3000,3000)矩阵相乘(ps:不要玩太嗨了,内存会爆满),如果是计算量很小的任务,两者差不多。

    但是,如果只是计算的话,速度好像都很快,接近0s,如果加上print,才有显著的区别。gpu为0.04s;cpu为2s。猜测一:print需要输出,gpu输出比cpu快;(经过测试,输出速度一致)猜测二:只计算的时候,使用了异步并行计算的方法,使得时间计算接近于0.

  • GPU上应如何读写模型参数?

    答:1.输入参数X记得ctx=mx.gpu();2.initialize记得ctx=mx.gpu();其他可以同正常那样操作。

最后更新于

这有帮助吗?