Mxnet -线性回归
由于,在Mxnet中使用了大量的函数来搭建模型结构,虽然简单,却同时看不出本来面目,不易理解。在本文中,比较《线性回归从零开始》与《线性回归的简介实现gluon》这两种方法,对比理解mxnet搭建模型的流程。并在《附录》中添加了运算过程中出现的函数解释,方便大家理解。
import mxnet
%matplotlib inlinezhi
from IPython import display # display 中设置 矢量图片
from matplotlib import pyplot as plt # 画图
from mxnet import autograd, nd # 导入 梯度、ndarray数据
import random # 导入随机包
一、线性回归从零开始
简单得理解线性回归得流程。
1.1 生成数据集
我们构造⼀个简单的⼈⼯训练数据集,它可以使我们能够直观⽐较学到的参数和真实的模型参数的区别。设训练数据集样本数为1000,输⼊个数(特征数)为2。给定随机⽣成的批量样本特征
X
∈
R
1000
×
2
X\in R^{1000×2}
X∈R1000×2,我们使⽤线性回归模型真实权重
w
=
[
2
,
−
3.4
]
T
w = [2, -3.4]^T
w=[2,−3.4]T和偏差
b
=
4.2
b = 4.2
b=4.2,以及⼀个随机噪声项
ϵ
ϵ
ϵ来⽣成标签
y
=
X
w
+
b
+
ϵ
y = Xw + b + ϵ
y=Xw+b+ϵ
其中噪声项ϵ服从均值为0、标准差为0.01的正态分布。噪声代表了数据集中⽆意义的⼲扰。下⾯,让我们⽣成数据集.
num_inputs = 2 # 输入特征
num_examples = 1000 # 样本个数
true_w = [2, -3.4] # 真实权重
true_b = 4.2 # 真是偏差
features = nd.random.normal(scale=1, shape=(num_examples, num_inputs))# 随机生成一个 1000*2结构得据
# featrues的每⼀⾏是⼀个⻓度为2的向量
labels = true_w[0] * features[:, 0] + true_w[1] * features[:, 1] + true_b # 通过真实线性关系构造标签
# labels的每⼀⾏是⼀个⻓度为1的向量(标量)。
labels += nd.random.normal(scale=0.01, shape=labels.shape) # 给标签增加一个噪声
features[0], labels[0] # 简单查看数据
查看线性关系
以第二个特征为例,通过⽣成第⼆个特征features[:, 1]和标签 labels 的散点图,可以更直观地观察两者间的 线性关系。
def use_svg_display():
# ⽤⽮量图显⽰
display.set_matplotlib_formats('svg') # 附录1
def set_figsize(figsize=(3.5, 2.5)):
use_svg_display()
# 设置图的尺⼨
plt.rcParams['figure.figsize'] = figsize
set_figsize() # 设置图行尺寸和矢量图格式
# 画散点图,大小s=1
plt.scatter(features[:, 1].asnumpy(), labels.asnumpy(), 1); #加分号只显⽰图 # 附录2
1.2 读取数据集
在训练模型的时候,我们需要遍历数据集并不断读取小批量数据样本。这⾥我们定义⼀个函数: 它每次返回batch_size
(批量⼤小)个随机样本的特征和标签。
def data_iter(batch_size, features, labels):
num_examples = len(features) # 特征个数
indices = list(range(num_examples)) # 特征个数索引列表
random.shuffle(indices) # 样本的读取顺序是随机的 # 附录3
for i in range(0, num_examples, batch_size):
j = nd.array(indices[i: min(i + batch_size, num_examples)])
# 切片得索引(包含最后没有达到batch_size得一个切片索引)
yield features.take(j), labels.take(j) # take函数根据索引返回对应元素 # 附录4
batch_size: 批量大小,整形
features: 特征, ndarray,shape=(1000*2)
labels: 标签ndarray,shape=(1000,)
分析:[如果改变成gluon中的函数呢]
- batch_size:用来决定切片的大小,和计算切片的索引。—保留参数batch_size
- 返回的数据是对应的切片的feature 和 labels --------feature 和labels 数据
- 中间进行了shuffle--------shuffle=True
根据批量产生数据并查看
让我们读取第⼀个小批量数据样本并打印。每个批量的特征形状为(10, 2),分别对应批量⼤小和
输⼊个数;标签形状为批量⼤小.
batch_size = 10
for X, y in data_iter(batch_size, features, labels):
print(X, y)
break #因为data_iter是一个迭代函数,如果不加的化,则会返回多个值
1.3 初始化模型参数
我们将权重初始化成均值为0、标准差为0.01的正态随机数,偏差则初始化成0
。
w = nd.random.normal(scale=0.01, shape=(num_inputs, 1))
b = nd.zeros(shape=(1,))
params=[w,b]
之后的模型训练中,需要对这些参数求梯度来迭代参数的值,因此我们需要创建它们的梯度
创建梯度 # 附录5(求导流程)
w.attach_grad() #w.grad 是附加到这个NDArray的梯度(属性)
b.attach_grad()
1.4 定义模型
下⾯是线性回归的⽮量计算表达式的实现。我们使⽤dot函数做矩阵乘法。
def linreg(X, w, b):
return nd.dot(X, w) + b # nd.dot() 点乘
1.5 定义损失函数
真实值y,预测值y_hat.两个ndarray的形状相同。定义平方损失。
def squared_loss(y_hat, y): #
return (y_hat - y.reshape(y_hat.shape)) ** 2 / 2
# y.reshape(y_hat.shape) 是将y变成y_hat 的形状
1.6定义优化算法
以下的sgd函数实现了上⼀节中介绍的小批量随机梯度下降算法。它通过不断迭代模型参数来优化损失函数。这⾥⾃动求梯度模块计算得来的梯度是⼀个批量样本的梯度和。我们将它除以批量⼤小来得到平均值.
根据梯度的定义:参数=参数-学习步长*总梯度/批量大小
def sgd(params, lr, batch_size):
for param in params:
param[:] = param - lr * param.grad / batch_size
params : 参数列表集合
lr :学习率
batch_size:批量大小
1.7 训练模型
在训练中,我们将多次迭代模型参数。
在每次迭代中,我们根据当前读取的小批量数据样本(特征X和标签y),通过调⽤反向函数backward
计算小批量随机梯度,并调⽤优化算法sgd迭代模型参数。
由于我们之前设批量⼤小batch_size为10,每个小批量的损失l
的形状为(10, 1)。回忆⼀下“⾃动求梯度” ⼀节。由于变量l
并不是⼀个量,运⾏l.backward()
将对l
中元素求和得到新的变量,再求该变量有关模型参数的梯度。
在⼀个迭代周期(epoch)中,我们将完整遍历⼀遍data_iter函数,并对训练数据集中所有样本都使⽤⼀次(假设样本数能够被批量⼤小整除)。这⾥的迭代周期个数num_epochs和学习率lr都是超参数,分别设3和0.03。在实践中,⼤多超参数都需要通过反复试错来不断调节。虽然迭代周期数设得越⼤模型可能越有效,但是训练时间可能过⻓
lr = 0.03 #学习率
num_epochs = 3 #轮次
net = linreg #线性回归 # 模型函数
loss = squared_loss #损失函数
for epoch in range(num_epochs): # 训练模型⼀共需要num_epochs个迭代周期
# 在每⼀个迭代周期中,会使⽤训练数据集中所有样本⼀次(假设样本数能够被批量⼤⼩整除)。
# X和y分别是⼩批量样本的特征和标签
for X, y in data_iter(batch_size, features, labels):
with autograd.record():
l = loss(net(X, w, b), y) # l是有关⼩批量X和y的损失
l.backward() # ⼩批量的损失对模型参数求梯度
sgd([w, b], lr, batch_size) # 使⽤⼩批量随机梯度下降迭代模型参数 # 优化器
train_l = loss(net(features, w, b), labels)
print('epoch %d, loss %f' % (epoch + 1, train_l.mean().asnumpy()))
out:
epoch 1, loss 0.054347 epoch 2, loss 0.000246 epoch 3, loss 0.000050
训练完成后,我们可以⽐较学到的参数和⽤来⽣成训练集的真实参数。它们应该很接近.
true_w, w
out:
([2, -3.4], [[ 1.9992912] [-3.3992891]] <NDArray 2x1 @cpu(0)>)
true_b, b
out:
(4.2, [4.1990314] <NDArray 1 @cpu(0)>)
二、线性回归的简洁实现
我们将介绍如何使⽤MXNet提供的Gluon接口更⽅便地实现线性回归的训练。
2.1 生成数据集
我们⽣成与上⼀节中相同的数据集。其中features是训练数据特征, labels是标签
%为了避免麻烦,我重启了一下
from mxnet import ndarray as nd
from mxnet import autograd
from mxnet import gluon
数据集的构造类似不变
num_inputs = 2
num_examples = 1000
true_w = [2, -3.4]
true_b = 4.2
features = nd.random.normal(scale=1, shape=(num_examples, num_inputs))
labels= true_w[0] * features[:, 0] + true_w[1] * features[:, 1] + true_b
labels += nd.random.normal(scale=0.01, shape=labels.shape)
2.2 读取数据集
Gluon提供了data包来读取数据。由于data常⽤作变量名,我们将导⼊的data模块⽤添加了Gluon⾸代替。在每⼀次迭代中,我们将随机读取包含10个数据样本的小批量。
生成批量数据的代码被统一成两个函数。
- 函数
gluon.data.ArrayDataset(features,labels)
将数据的特征和标签组合。- 函数
gluon.data.Dataloader(dataset,batch_size, shuffle=True)
读取批量数据
batch_size = 10
# 将训练数据的特征和标签组合
dataset = gluon.data.ArrayDataset(features, labels)
# 随机读取⼩批量
data_iter = gluon.data.DataLoader(dataset, batch_size, shuffle=True)
这⾥data_iter的使⽤与上⼀节中的⼀样。让我们读取并打印第⼀个小批量数据样本
for data, label in data_iter:
print(data,label)
break
2.3 定义模型
Gluon提供了⼤量预定义的层,这使我们只需关注使⽤哪些层来构造模型。下⾯将介绍如何使⽤Gluon更简洁地定义线性回归。
⾸先,导⼊nn模块
。实际上,“nn”是neural networks(神经⽹络)的缩写。顾名思义,该模块定义了⼤量神经⽹络的层。我们先定义⼀个模型变量net,它是一个Sequential(顺序的?)实例。在Gluon中,Sequential实例可以看作是⼀个串联各个层的容器造模型时,我们在该容器中依次添加层。当给定输⼊数据时,容器中的每⼀层将依次计算并将输出作为下⼀层的输⼊。
构建模型最简单的方法就是利用Sequential
来把所有层串起来。首先我们定义一个空的模型
from mxnet.gluon import nn
net = nn.Sequential()
然后我们加入一个Dense层,它唯一必须要定义的参数就是输出节点的个数,在线性模型里面是1.因为后面是输出的节点的个数,我们知道,y=wx+b,有1个y,所以,输出为1.
net.add(nn.Dense(1)) # net.add() 在nn.Sequential中添加# 附录6 # nn.Dense() 密集层,全连接层 # 附录7
net
out:
Sequential( (0): Dense(None -> 1, linear) )
⼀个单层神经⽹络,线性回归输出层中的神经元和输⼊层中各个输⼊完全连接。因此,线性回归的输出层⼜叫全连接层。在Gluon中,全连接层是⼀个Dense实例。我们定义该层输出个数为1。
值得⼀提的是,在Gluon中我们⽆须指定每⼀层输⼊的形状,例如线性回归的输⼊个数。当模型得到数据时,例如后⾯执⾏net(X)时,模型将⾃动推断出每⼀层的输⼊个数。我们将在之后“深度学习计算”⼀章详细介绍这种机制。 Gluon的这⼀设计为模型开发带来便利。
2.4 初始化模型参数
在使⽤net前,我们需要初始化模型参数,如线性回归模型中的权重和偏差。我们从MXNet导⼊init模块
。该模块提供了模型参数初始化的各种⽅法。这⾥的init是initializer的缩写形式。我们通过init.Normal(sigma=0.01)指定权重参数每个元素将在初始化时随机采样于均值为0、标准差为0.01的正态分布。偏差参数默认会初始化为零。
from mxnet import init
net.initialize(init.Normal(sigma=0.01))
#net.initialize() 默认方式的初始化
2.5 定义损失函数
在Gluon中, loss模块定义了各种损失函数。直接 使⽤它提供的平⽅损失作为模型的损失函数。
from mxnet.gluon import loss as gloss
loss = gloss.L2Loss() # 平⽅损失⼜称L2范数损失
2.6 定义优化算法
同样,我们也⽆须实现小批量随机梯度下降。在导⼊Gluon后,我们创建⼀个Trainer实例
,并指定学习率为0.03
的小批量随机梯度下降(sgd)
为优化算法。该优化算法将⽤来迭代net实例所有通过add函数嵌套的层所包含的全部参数。这些参数可以通过collect_params函数
获取。
trainer = gluon.Trainer(net.collect_params(), 'sgd', {'learning_rate': 0.03})
#Trainer(params, optimizer, optimizer_params=None, kvstore='device', compression_params=None, update_on_kvstore=None)
#第一个是参数,第二个是优化器,后面的字典是优化器的参数
2.7 训练模型
在使⽤Gluon训练模型时,我们通过调⽤Trainer实例的step函数
来迭代模型参数。
上⼀节中我们提到,由于变量l
是⻓度为batch_size的⼀维NDArray,执⾏l.backward()
等价于执⾏l.sum().backward()
。按照小批量随机梯度下降的定义,我们在step函数
中指明批量⼤小,从而对批量中样本梯度求平均.
num_epochs = 3
for epoch in range(1, num_epochs + 1):
for X, y in data_iter: # 读取数据
with autograd.record(): # 对参数添加梯度属性,开辟空间
l = loss(net(X), y) # 计算损失
l.backward() #向后 # 对小批量求梯度(是一个sum的状态)
trainer.step(batch_size)# trainer 是一个优化算法,利用step函数中的批量大小,求平均梯度
# 每一轮迭代得到一个模型,计算该模型下 预测值net(features) 和 真实值labels 的损失
l = loss(net(features), labels)
print('epoch %d, loss: %f' % (epoch, l.mean().asnumpy()))
验证
下⾯我们分别⽐较学到的模型参数和真实的模型参数。我们从net获得需要的层,并访问其权重(weight)和偏差(bias)。学到的参数和真实的参数很接近。
dense = net[0]
true_w, dense.weight.data()
out:
([2, -3.4], [[ 1.9994873 -3.3995135]] <NDArray 1x2 @cpu(0)>)
true_b, dense.bias.data()
out:
(4.2, [4.1993074] <NDArray 1 @cpu(0)>)
附录
1. display.set_matplotlib_formats('svg')
- 功能描述:选择内联后端的图形格式。(可选)通过JPEG的质量。
# 例如,这将启用PNG和JPEG输出,且JPEG质量为90%
set_matplotlib_formats('png', 'jpeg', quality=90)
- format(格式)种类:
'png','retina','jpeg','svg','pdf'
2.plt.scatter()
- 功能描述: 化散点图
scatter(x, y, s=None, c=None, marker=None, cmap=None, norm=None, vmin=None, vmax=None, alpha=None, linewidths=None, verts=None, edgecolors=None, *, plotnonfinite=False, data=None, **kwargs)
3.random.shuffle
- 功能描述:shuffle() 方法将序列的所有元素随机排序
import random
random.shuffle (lst )
- 参数:可以是一个列表
- 返回值:没有返回值
4.nd.take()
-
功能描述:沿给定轴从输入数组中获取元素.
此函数使用提供的索引沿特定轴对输入数组进行切片
take(a=None, indices=None, axis=_Null, mode=_Null, out=None, name=None, **kwargs)
5.求导数的流程
-
需要先调⽤
attach_grad
函数来申请存储梯度所需要的内存 -
为了减少计算和内存开销,默认条件下MXNet不会记录⽤于求梯度 的计算。我们需要调⽤
record
函数来要求MXNet记录与求梯度有关的计算。 -
接下来我们可以通过调⽤
backward
函数⾃动求梯度。需要注意的是,如果y不是⼀个标量, MXNet将默认先对y中元素求和得到新的变量,再求该变量有关x的梯度。 -
MXNet的 运 ⾏ 模 式 包 括 训 练 模 式 和 预 测 模 式。 我 们 可 以 通 过
autograd.is_training()
来判断运⾏模式。
6.net.add()
- 功能描述:创建并返回一个新对象。
7.nn.Dense
- 功能描述:密集层
output = activation(dot(input, weight) + bias)
其中,activation
是逐元素激活函数- 作为‘activation’参数传递时,'weight’时权重矩阵由该层创建,
- ’bias’是偏差向量由该层创建,(仅在“ use_bias”为“ True”时适用)
class Dense(mxnet.gluon.block.HybridBlock) Dense(units, activation=None, use_bias=True, flatten=True, dtype='float32', weight_initializer=None, bias_initializer='zeros', in_units=0, **kwargs)
- 参数
units: 输出空间的维数(该层的神经元节点数)
acivation:激活函数;如果您未指定任何内容,则不会应用任何激活。(即“线性”激活:“ a(x)= x”)。
use_bias:boolean型,是否使用偏置项
flatten:输入张量是否应该展平。如果为true,则除第一数据轴外的所有数据都会合拢在一起。如果为false,则输入数据的除最后一个轴外的所有轴均保持不变,并进行转换应用于最后一个轴。
dtype:输出嵌入的数据类型。
weight_initializer:’kernel’权重矩阵初始化
bias_initializer:偏差向量的初始化器
in_units:输入数据的大小。
- 方法解析顺序
1.从mxnet.gluon.block.HybridBlock继承的方法
cast(self, dtype)
\export(self, path, epoch=0, remove_amp_cast=True)
\forward(self, x, *args)
\hybridize(self, active=True, **kwargs)
\infer_shape(self, *args)
\infer_type(self, *args)
\register_child(self, block, name=None)
\register_op_hook(self, callback, monitor_all=False)
\ \2. 从 mxnet.gluon.block.Block继承的方法
apply(self, fn)
\collect_params(self, select=None)
\initialize(self, init=<mxnet.initializer.Uniform object at 0x0000026C81339108>, ctx=None, verbose=False, force_reinit=False)
\load_parameters(self, filename, ctx=None, allow_missing=False, ignore_extra=False, cast_dtype=False, dtype_source='current')
\register_forward_hook(self, hook)
\register_forward_pre_hook(self, hook)
\save_parameters(self, filename, deduplicate=False)
\save_params(self, filename)
\summary(self, *inputs)
\ \