《动手学深度学习》-深度学习计算
在我学习动手学深度学习这本书的这一章时,它的顺序是:模型的构造、模型参数的访问初始化与共享、模型参数延后初始化、自定义层、读取与存储、GPU计算。
但是在我学习的构造模型的时候,它的正向传播函数用到了层,但是我却不知道层是怎么定义的。在访问模型的时候我会想它是怎么初始化的。所以在这片博客中我按照:模型参数的延后初始化、模型参数的访问初始化与共享、自定义层、模型的构造、读取与存储、GPU计算。可以先按照首段的顺序学习,捋一遍的时候参照我的顺序捋一遍就会清楚很多。
我是自学的,并没有老师可以询问。所以里面有些细节、小感悟可能会有些错误,请酌情参考。错误之处还望不吝指正。
模型的延后初始化
众所周知,mxnet的一大特色就是延后初始化。**在定义模型后并不知道w的值与他们的形状,只有在模型进行向前计算后,系统才根据输入的形状判断权重参数的值,**然后才进行上几行代码中的initialize()进行初始化。
from mxnet import init, nd
from mxnet.gluon import nn
class MyInit(init.Initializer):
#重写了Initializer的初始化权重函数
def _init_weight(self, name, all):
print("Init", name, all.shape);
x = nd.random.uniform(shape = (2, 10))
net = nn.Sequential()
net.add(nn.Dense(100, activation="relu"),
nn.Dense(20))
net.initialize(init=MyInit())
print("up or down?")
net(x)
若在initialize()处初始化则,信息会在up or down之上,否则反之。结果是:
Init dense0_weight (100, 10)
Init dense1_weight (20, 100)
up or down?
细心的同学可能会问,为什么name是什么,为什么形状会是(100,10)不是应该是(10,100)吗?
首先,name是collect_params中所有weight的key(collect_params是个字典,下面参数的访问中会讲到。好像改成这个顺序也有点乱(哭笑),但是在我学习参数访问时不知道怎么延后初始化的所以也乱)在之前学习softmax回归时,列才表示的是输出个数。我个人认为它的这种格式更利于延后初始化,因为已定的数组是不可以改变维度的。
模型进行一次向前计算后才可以访问参数,但是有时我们不想这么做。当然!延后初始化是可以避免的。
- 因为第一次初始化过,所以参数的形状固定了可以直接进行重新初始化,这是不需要再次进行向前计算的
net.initialize(init=MyInit(), force_reinit=True)
#force_reinit,若已初始化,是否强制重新初始化
Init dense0_weight (100,10)
Init dense1_weight (10,100)
- 初始化之前,固定了每层的输入,达到关闭延后初始化的效果。
#in_units是输入大小,units是输出大小就是神经元数
net.add(nn.Dense(units=100, in_units=30, activation="relu"),
nn.Dense(units=20, in_units=100))
net.initialize(init=MyInit())
#这时直接就初始化,输出了。但是你要注意形状的关系,关闭了延时,这就是你要考虑的事情了。
Init dense0_weight (100, 30)
Init dense1_weight (20, 100)
模型参数的访问、初始化、共享
模型参数的访问
很多时候我们想看看训练好的模型的参数是个什么亚子,或者初始化成什么亚子。因为参数是封装在Parameter类中的,所以就让我们看看怎么访问吧。
在Block类中有个属性,prefix。应该是个静态成员,没创建一个对象+1。name是由prefix+weight或bias组成的。
这是个大概的组成。
from mxnet import init, nd
from mxnet.gluon import nn
x = nd.random.uniform(shape = (10, 10))
net = nn.Sequential()
net.add(nn.Dense(20, activation="relu"),
nn.Dense(10))
net.initialize()
net(x)
#访问,某一层可以通过使用[],比如第0层。
print(net[0].params)
print(net[0].weight, net[0].params['dense0_weight'])
print(net[0].bias)
#Parameter对象通过.data()访问数据
print(net[0].weight.data()[0])
print(net.params)
dense0_ (
Parameter dense0_weight (shape=(20, 10), dtype=float32)
Parameter dense0_bias (shape=(20,), dtype=float32)
)
#第二个输出俩一样,所以在这我就写了一个
Parameter dense0_weight (shape=(20, 10), dtype=float32)
Parameter dense0_bias (shape=(20,), dtype=float32)
[ 0.01958894 0.01148278 -0.04993054 0.00523225 0.06225365 0.03620619
0.00305876 -0.05517294 -0.01194733 -0.00369594]
dense0_ (
)
思考一下,为什么net也是Block的子类对象params却为空,因为它里面没有定义参数,这就涉及到了层的定义,所以学习的时候来回穿插很心累(哭笑)
collect_params访问模型以及其中的层的所有参数
print(net[0].collect_params)
print(net.collect_params)
#也可以使用正则表达式
print(net.collect_params(".*weight"))
dense0_ (
Parameter dense0_weight (shape=(20, 10), dtype=float32)
Parameter dense0_bias (shape=(20,), dtype=float32)
)
sequential0_ (
Parameter dense0_weight (shape=(20, 10), dtype=float32)
Parameter dense0_bias (shape=(20,), dtype=float32)
Parameter dense1_weight (shape=(10, 20), dtype=float32)
Parameter dense1_bias (shape=(10,), dtype=float32)
)
sequential0_ (
Parameter dense0_weight (shape=(20, 10), dtype=float32)
Parameter dense1_weight (shape=(10, 20), dtype=float32)
)
模型参数的初始化
在参数访问例子中,initialize()函数的没有参数,使用的是默认的uniform随机初始化。但是也可以自定义初始化方式。
#Block的子类调用的(net.initialize()),下面是源码
def initialize(self, init=initializer.Uniform(), ctx=None, verbose=False,force_reinit=False):
self.collect_params().initialize(init, ctx, verbose, force_reinit)
#params调用的(↑collect_params().initialize(init, ctx, verbose, force_reinit))
def initialize(self, init=None, ctx=None, default_init=initializer.Uniform(),force_reinit=False):
if self._data is not None and not force_reinit:
warnings.warn("Parameter '%s' is already initialized, ignoring. " \
"Set force_reinit=True to re-initialize." % self.name,
stacklevel=2)
return
self._data = self._grad = None
if ctx is None:
ctx = [context.current_context()]
if isinstance(ctx, Context):
ctx = [ctx]
if init is None:
init = default_init if self.init is None else self.init
if not shape_is_known(self.shape):
if self._allow_deferred_init:
self._deferred_init = (init, ctx, default_init, None)
return
raise ValueError("Cannot initialize Parameter '%s' because it has " \
"invalid shape: %s." % (self.name, str(self.shape)))
self._deferred_init = (init, ctx, default_init, None)
self._finish_deferred_init()
#这是Initialize类的函数,上面是Block的。默认传的是Uniform类对象,这是重写好的。
def _init_weight(self, name, arr):
raise NotImplementedError("Must override it")
init中提供了多种初始化方法:
- init.Normal(sigma=0.01),正态分布初始化
- init.Constant(1),初始化为常数1
- init.Xavier()
可以对net进行初始化,所有的参数将初始化。也可对单个的Parameter对象初始化,与模型初始化相同。
net.initialize(init=init.Normal(sigma=0.01), force_reinit=True)
net[0].weight.initialize(init=init.Constant(2), force_reinit=True)
除了使用现成的初始化方法,也可以使用自定义初始化方法。继承自Initialize类。
class MyInit(init.Initialize):
#这个在自定义类中,必须重写,可以看看上面的源码。name就是参数的名字,all是data。这是初始化一个参数的函数
def _init_weight(self, name, all):
#shape不可以变的,在此之前形状以确定,这只是延后初始化的方案,不是延后初始化
all[:]=nd.random.uniform(low=-10, high=10, shape=data.shape)
data *= data.abs() >= 5
#初始化为[-10,-5][5,10]之间的数
net.initialize(init=MyInit())
net(x)
Parameter类有set_data()可以直接对参数进行改写
net[0].weight.set_data(net[0].weight.data() + 1)
模型参数的共享
不同层使用同一层参数。
net = nn.Sequential()
shared = nn.Dense(8, activation="relu")
net.add(nn.Dense(8, activation="relu"),
shared,
nn.Dense(8), params=shared.params,
nn.Dense(10))
#第二层与第三层共享参数,但是是不同层因为Dense的name不同。但是也相当于一层,若激活函数相同的话,因为参数相同、激活函数相同。输出值就是一样的方法计算的,而且优化函数也是一样的计算方法,因为w、b使用同一组。
#注意:使用同一组参数的话输出要相同。
在模型的构造中会介绍,使用同一层作为两层(绕口,我也不知道怎么形容,意会意会(笑))
自定义层
- 不含模型参数的自定义层:
#通过继承Block创建层
class Layer(nn.Block):
def __init__(self, **kwargs):
#初始化父类
super(Layer, self).__init__(**kwargs)
def forward(self, x):
return x - x.mean()
- 含模型参数的自定义层:
class MyDense(nn.Block):
def __init__(self, **kwargs):
super(MyDense, self).__init__(**kwargs)
#在字典中查有无weight若有返回,无加上前缀放入字典。注意这个形状是不是与nn就有的层形状相反。自定义时写成这样好计算。
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.dara()) + self.bias.data()
return relu(linear)
你自己定义的形状注意确定,有能力也可以写成延时初始化。
模型的构造
模型与层都是相同的东西,都是继承自Block类,都是由输入、输出、向前计算撑起来的。只不过层比模型更小,在forward中是最底层的计算而不是单纯的(x),是吧x与w进行运算后输出的。层是模型的基本单位。
class MLP(nn.Block):
def __init__(self, **kwargs):
#初始化父类
super(MLP, self).__init__(**kwargs)
#定义模型,但是这种直接固定死了
self.hidden = nn.Dense(256, activation="relu")
self.output = nn.Dense(20)
#在对象像函数一样用函数调用时,首先调用__call__函数,然后call函数调用重写后的你这个forward函数
def forward(self, x):
return self.output(self.hidden(x))
class MySequential(nn.Block):
def __init__(self, **kwargs):
super(MySequential, self).__init__(**kwargs)
def add(self, *args):
#将模型或者层添加进来,放到args中,每个block都有自己的名字,name是个静态属性在随着Block子类对象的创建在增加
#将block放入父类的字典_children中(它是一个顺序字典,按照添加顺序排列)
for block in args:
self._children[block.name] = block
def forward(self, x):
#从Orderdict中按照上面的添加顺序,读取字典的值(也就是你的模型或层)
for dence in self._children.values():
x = dence(x)
return x
'''这是一个更复杂的MPL,有多层共用一组权重,与常数dot,以及控制流'''
class FancyMPL(nn.Block):
def __init__(self, **kwargs):
super(FancyMPL, self).__init__(**kwargs)
'''
这是get_constant的源码
---------------------------------------------------------------------------
name = self.prefix + name
param = self._get_impl(name)
if param is None:
if value is None:
raise KeyError("No constant named '{}'. Please specify value " \
"if you want to create a new constant.".format(
name))
param = Constant(name, value)
self._params[name] = param
---------------------------------------------------------------------------
'''
#在父类的params中若有名为rand_weight的参数则返回其参数,若没有将它名字加个前缀加入params然后返回value
#value是nd.random.uniform(shape=(20, 20))
self.rand_weight = self.params.get_constant("rand_weight", nd.random.uniform(shape=(20, 20)))
self.dense = nn.Dense(20, activation="relu")
def forward(self, x):
#输出一个(2,20)
x = self.dense(x)
#在这里一定要注意,rand_weight的shape是(20,20)的;且第0维度必须是20,因为它要与dense的输出做dot运算
#第一个维度的20是一个样本的输出个数
#赋值后的x为(2,20)
x = nd.dot(x, self.rand_weight.data()) + 1
#重复调用,两层公用一组权重
x = self.dense(x)
while x.norm().asscalar() > 1:
x /= 2
if x.norm().asscalar() < 0.8:
x *= 10
return x.sum()
from mxnet.gluon import nn
from mxnet import nd
from tectonic_model.fancyMPL import FancyMPL
from tectonic_model.model1 import MLP
x = nd.random.uniform(shape=(2, 20))
net = nn.Sequential()
#将自己构造的模型作为一层添加到net中,形成更复杂的模型,这就是灵活性的体现
net.add(MLP(), FancyMPL())
net.initialize()
print(net.params, net.collect_params())
print(net(x))