mxnet出了gluon好久了,沐神也出了一系列精品课程,这么好的东西就系统的看了一遍教程。做了如下笔记,希望以后回顾的时候能有些用处。
另外,gluon有自己的官方中文社区,而且有大牛坐镇,推荐!
如解浚源知乎中所言:
Gluon是MXNet的动态图接口;Gluon学习了Keras,Chainer,和Pytorch的优点,并加以改进。接口更简单,且支持动态图(Imperative)编程。相比TF,Caffe2等静态图(Symbolic)框架更加灵活易用。同时Gluon还继承了MXNet速度快,省显存,并行效率高的优点,并支持静、动态图混用,比Pytorch更快。
NDArray 是 MXNet用于储存和变化数据的主要工具。使用方法与Numpy相似。
这里只写一下数据替换操作
from mxnet import ndarray as nd
x=nd.ones((3,4))
y=nd.ones((3,4))
y=y+x #此时等号左右两边的y不是同一个id
#下面两种方法可避免新内存开销
y[:]=y+x#此时等号左右两边相同
y+=x#此时等号左右两边相同
###
z=nd.zeros_like(x)
z[:]=x+y #此时z的id相同,但是仍有额外临时x+y的开销,下面方法可避免
nd.elemwise_add(x,y,out=z)
mxnet.autograd可以方便对算式记性求导,使用方法:
import mxnet.ndarray as nd
import mxnet.autograd as ag
# f=2*x*x
x=nd.array([[1,2],[3,4]])
x.attach_grad()# 对x求导,先开辟导数所需要的空间
with ag.record():
y=2*x*x
y.bachward()
print(x.grad)
另外还可以将链式求导的head gradient 显示的作为backward的参数
这节的意义在于手动实现一遍使用SGD求解线性回归的过程。对于新手还是挺重要的。
所谓线性回归就是对于数据x及目标y,求映射参数w和b使得,yhat=xw+b
与y的值尽量接近,即sum((yhat-y)^2)
最小,求解伪代码如下:
for batch(x,y) in data_iter():
with autograd.record():
yhat=dot(x,w)+b# x,w需要事先attach_grad
loss=sum((yhat-y)^2)
loss.backward()
for p in [w,b]:
w[:]=w-lr*w.grad#注意替换的用法哦; 这里的grad其实是batch数据的sum,sgd中应该使用平均
下面是gluon的代码
net=gluon.nn.Sequential()
net.add(gluon.nn.Dense(1))#指明输出数量为1
net.initialize()#初始化时默认随机初始化
square_loss=gluon.loss.L2Loss()
trainer=gluon.Trainer(
net.collect_params,
'sgd',
{'learning_rate':0.1})
###############
epochs = 5
batch_size = 10
for e in range(epochs):
total_loss = 0
for data, label in data_iter:
with autograd.record():
output = net(data)
loss = square_loss(output, label)
loss.backward()
trainer.step(batch_size)
total_loss += nd.sum(loss).asscalar()
print("Epoch %d, average loss: %f" % (e, total_loss/num_examples))
对于dense的权重访问:
net[0].weight.data();net[0].bias.data()
例子中使用多分类逻辑回归对Fashion MNIST数据集进行分类。
对于分类如果使用线性回归,得到对于输入数据x的线性映射,这时候不能很好的表示类别,为了将映射结果转为类别表示,就用了softmax即yhat=exp(x)/exp(x).sum(axis=1,keepdims)
(为了避免overflow,通常会在exp之前减去最大值)
为了表示分类的效果,使用交叉熵损失表示即loss=-log(yhat)
yhat为对应的y为1时的值,此时如果yhat为1,则得到的损失为0.
有了yhat和loss,替换2.1中的伪代码即可。
gluon实现:
net=gluon.nn.Sequential()
with net.name_scope():#name scope便于命名
net.add(gluon.nn.Flatten())
net.add(gluon.nn.Dense(10))
net.initialize()
softmax_cross_entropy=gluon.loss.SoftmaxCrossEntropyLoss()
trainer=gluon.Trainer(net.collect_params(),'sgd',{'learning_rate:0.1})
#后续autograd.record();backward;trainer.step(batch_size);不再赘述
类似于2.2,此处yhat的得到时多了一各求中间隐含层的过程。gluon中net的多了个一各Dense层
net=gluon.nn.Squential()
with net.name_scope():
net.add(gluon.nn.Dense(256,activtion='relu'))
net.add(gluon.nn.Dense(10))
net.initialize()
欠拟合与过拟合是机器学习模型训练过程中必须要知道的概念,欠拟合是指模型还没有充分的从训练数据集中学到东西,过拟合是指对于数据集中细枝末节过于学习,造成了不能get到更有代表性的特征,解决过拟合的方法有很多比如添加正则项、增加训练数据、ensemble、dropout等等。
教程中关于欠拟合和过拟合的例子非常好。
教程中使用输入数据维度较大,训练样本比较少的时候,查看了正则化的好处,所谓正则化就是在loss中增加参数(通常只是w)的范数,直观上来说不能使权重过大,这样可以使分类界限更加平滑。
贝叶斯的角度,l2正则是先验为高斯分布下的后验最大估计,l1正则是先验为拉普拉斯分布下的后验最大估计
gluon实现的时候,在trainer中加入weight_decay项
trainer = gluon.Trainer(
net.collect_params(), 'sgd',
{'learning_rate': learning_rate, 'wd': weight_decay})
dropout可以看成每次随机 扔掉某一层的某一些输入数据,使用部分输入用以学习特征,可以看成ensemble的一种变种,注意的是只在训练过程中使用,且输入数据在去掉后要扩大相应的倍数,以使前传与训练时输入分布相似。
gluon中的使用方法如下:
net=nn.Sequential()
with net.name_scope():
net.add(nn.Flatten())
net.add(nn.Dense(256,activation='relu'))
net.add(nn.Dropout(0.2))
net.add(nn.Dense(256,activation='relu'))
net.add(nn.Dropout(0.5))
net.add(nn.Dense(10))
net.initialize()
简单来说正向传播计算loss,反向传播利用loss计算各参数的梯度,用梯度来更新参数
Gluon中主要使用Block来构造模型,Block是Gluon里的一个类,位于incubator-mxnet/python/mxnet/gluon/block.py
,前面使用的Sequential是他的一个子类,位于incubator-mxnet/python/mxnet/gluon/nn/basic_layers.py
。
使用Block无需定义求导或反传函数,MXNet会使用autograd对forward自动生成相应的backward函数。
class MLP(nn.block):
def __init__(self,**kwargs):
super(MLP,self).__init__(**kwargs)
with self.name_scope():
self.hidden =nn.Dense(256,active='relu')
self.output =nn.Dense(10)
def forward(self,x)
return self.output(self.hidden(x))
使用的时候只需要实例化之后initialize一下赋予输入值即可进行一次forward:
net=MLP()
net.initialize()
class NestMLP(nn.Block):
def __init__(self,**kwargs):
super(NestMLP,self).__init__(**kwargs)
self.net = nn.Squential()
with self.name_scope():
self.net.add(nn.Dense(64,activation='relu'))
self.net.add(nn.Dense(32,activation='relu'))
self.dense=nn.Dense(16,activation='relu')
def forward(self,x):
return self.dense(self.net(x))
###
net=nn.Sequential()
net.add(NestMLP())
net.add(nn.Dense(10))
net.initialize()
值得注意的是使用block时,定义的网络需要为block类型,如self.denses = [nn.Dense(256), nn.Dense(128), nn.Dense(64)]
赋值为list不是block会出现无法注册的问题
在Gluon 中,模型参数的类型是Parameter。
collect_params 来访问Block ⾥的所有参数。
使用Siamese或者其他网络时可能需要共享参数,这时候只需要通过Block 的params
来指定模型参数即可如:
net.add(nn.Dense(4, activation='relu', params=net[1].params))
#或者
self.hidden3 = nn.Dense(4, activation='relu',params=self.hidden2.params)
搞深度自定义层还是经常用到的
这里只列出简单的自定义层的方法,与使用Block构建模型相似:
class mydense(nn.Block):
def __init__(self,uints,in_units,**kwargs):#uints,in_uints分别是输出单元个数及输入单元个数
super (mydense,self).__init__(**kwargs)
with self.name_scope():
self.weight=self.params.get('weight',shape=(in_uints,uints))
self.bias=self.params.get('bias',shape=(uints,))
def forward(self,x):
linear=nd.dot(x,self.weight.data())+self.bias.data()
return nd.relu(linear)
自己定义延迟初始化层稍微麻烦点,建议看下gluon源代码。需要hybridize,以及intializer延后调用
block支持直接使用save_params
及load_params
记性模型的写与读:
net1.save_params('./net1.params')
net2.load_params('./net1.params')
mx.cpu(), mx.gpu()
可以用来指定ctx
如net.initialize(ctx=mx.gpu())
可以使用copyto 和as_in_context
函数在CPU/GPU 之间传输数据,如果源变量和⽬标变量的context ⼀致,as_in_context 使⽬标变量和源变量共享源变量的内存,而copyto 总是为⽬标变量新创建内存。
y = x.copyto(mx.gpu())
z = x.as_in_context(mx.gpu())
打印NDArray 或将NDArray 转换成NumPy 格式时,MXNet 会⾃动将数据复制到主
内存
命令式编程比较灵活,符号式编程效率较高且容易部署,Gluon使用混合编程,集两者之长
在混合式编程中,我们可以通过使⽤HybridBlock 或者HybridSequential 类构建模型。默认情况下,它们和Block 或者Sequential类⼀样依据命令式编程的⽅式执⾏。当我们调⽤hybridize 函数后,Gluon 会转换成依据符号式编程的⽅式执⾏。事实上,绝⼤多数模型都可以享受符号式编程的优势。
使用方法与Sequential类似
net=nn.HybridSequential()
with net.name_scope():
net.add(
nn.Dense(256,activation='relu'),
nn.Dense(256,activation='relu'),
nn.Dense(2,activation='relu')
)
net.initialize()
net.hybridize()#通过调⽤hybridize 函数来编译和优化HybridSequential 实例中串联的层的计算。
net(x)
net.export('sy')#export 函数保存符号式程序和模型参数
class HybridNet(nn.HybridBlock):
def __init__(self,**kwargs):
super(HybridNet,self).__init__(**kwargs)
with self.name_scope():
self.hidden=nn.Dense(10)
self.output=nn.Dense(2)
def hybrid_forward(self,F,x):#多了一个F,命令编程时F为NDArray,符号式编程时为Symbol(mxnet中的符号式程序类)
x=F.relu(self.hidden(x))
return self.output(x)
net=HybridNet()
net.initialize()
net.hybridize()#####
net(x)
调用hybridize 函数后运⾏net(x) 的时候,得到符号式程序,之后再运⾏net(x)的时候MXNet 将不再访问Python 代码,而是直接在C++ 后端执⾏符号式程序。
所谓惰性计算简单点说就是用到啥再算啥
MXNet 包括⽤⼾直接⽤来交互的前端和系统⽤来执⾏计算的后端,⽤⼾可以使⽤不同的前端语⾔编写MXNet 程序,像Python、R、Scala 和C++。⽆论使⽤何种前端编程语⾔,MXNet 程序的执⾏主要都发⽣在C++ 实现的后端。换句话说,⽤⼾写好的前端MXNet 程序会传给后端执⾏计算。后端有⾃⼰的线程来不断收集任务,构造、优化并执⾏计算图。
MXNet 后端将默认使⽤惰性计算来获取最⾼的计算性能。
如果想让后端结果与前端同步可以使用wait_to_read
或nd.waitall
来等待后端计算,同时像print/ asnumpy /asscalar
等也可以实现同步。
惰性计算会优化计算效率,但是可能会增加使用内存,所以:
由于深度学习模型通常⽐较⼤,而内存资源通常有限,我们建议读者在训练模型时对每个小批量都使⽤同步函数。类似地,在使⽤模型预测时,为了减小内存开销,我们也建议读者对每个小批量预测时都使⽤同步函数,例如直接打印出当前批量的预测结果。
同时又cpu和gpu程序运行时会mxnet会自动优化(包括从CPU到GPU的复制数据),而非两个程序顺序执行。
Gluon 提供了split_and_load
函数。它可以划分⼀个小批量的数据样本并
复制到各个CPU/GPU 上,当我们使⽤多个GPU 来训练模型时,gluon.Trainer 会⾃动做数据并⾏,可以划分一个batch数据样本并复制到各个GPU 上,对各个GPU 上的梯度求和再同步到所有GPU 上。
sce_loss = gluon.loss.SoftmaxCrossEntropyLoss()
ctx=[mx.gpu(i) for i in range(yourdevice)]
net.collect_params().initialize(init=init.Xavier(),ctx=ctx,force_reinit=True)# 多卡同时初始化
trainer=gluon.Trainer(net.collect_params(),'sgd',{'learning_rate':lr})
for epoch in range(1,epoch_num):
total_loss=0
for feature,label in train_data:
gpu_data=gluon.utils.split_and_load(feature,ctx)
gpu_label=gluon.utils.split_and_load(label,ctx)
with autograd.record():
losses=[sec_loss(net(x),y)for x,y in zip(gpu_data,gpu_labels)]
total_loss+=sum([loss.sum().asscalar() for loss in losses])
trainer.step(batch_size)
nd.waitall()
mxnet提供了image工具
from mxnet import image
img = image.imdecode(open('../img/cat1.jpg', 'rb').read())
augs = [image.HorizontalFlipAug(.5),\
image.RandomCropAug([200,200]),\
image.RandomSizedCropAug((200,200), .1, (.5,2))\#随机裁剪,要求保留⾄少0.1 的区域,随机长宽⽐在.5 和2 之间。
image.BrightnessJitterAug(.5)\#随机将亮度增加或者减⼩在0-50% 间的⼀个量
aug = image.HueJitterAug(.5)#随机⾊调变化
]
for f in augs:
img = f(img)
conv0 = gluon.nn.Conv2D(我的参数)
conv0.bias.lr_mult = 2.
net = gluon.nn.Sequential()
net.add(conv0)
#或者
net[k].bias.lr_mult = 2.
gluon.data.DataLoader
自带多进程读取数据
到这里就基本会使用gluon了。教程之后从AlexNet 、VGG、 GoogLeNet 讲到ResNet、DenseNet。还有计算机视觉中的检测、分割入门,自然语义处理、时间序列、GAN等等方面的入门知识,非常适合刚刚深度学习的同学用来学习,这里就不展开了。
从零开始系列也是非常的利于理解基础知识