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)
系统将通过自动求梯度而自动生成反向传播所需的backward
函数。
Sequential类继承自Block类,成员变量_children里,其类型是OrderedDict。当MySequential实例调用initialize函数时,系统会自动对_children里所有成员初始化.
self.params.get_constant创建的随机权重参数不会在训练中被迭代.
都是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))
练习
尝试在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计算
所有nd有关的变量的初始化,记得加上ctx=mx.gpu()
net.initialize()时,记得加上ctx=mx.gpu()
y = x.copyto(mx.gpu()) # 拷贝到GPU,耗内存
z = x.as_in_context(mx.gpu()) # 转换到GPU
cpu和gpu上的变量不能同时运算,不同gpu上的也不能运算,因为I/O耗费时间。
练习
试试大一点儿的计算任务,如大矩阵的乘法,看看使用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();其他可以同正常那样操作。