尽管强⼤的深度学习框架可以减少⼤量重复性⼯作,但若过于依赖它提供的便利,会导致我们很难深⼊理解深度学习是如何⼯作的。因此,本节将介绍如何只利⽤NDArray和autograd来实现⼀个线性回归的训练。
⾸先,导⼊本节中实验所需的包或模块
from IPython import display
from mxnet import autograd, nd
import random
1.生成数据集
我们构造⼀个简单的⼈⼯训练数据集,它可以使我们能够直观⽐较学到的参数和真实的模型参数的区别。设训练数据集样本数为1000,输⼊个数(特征数)为2。给定随机⽣成的批量样本特征X ∈ R1000×2,我们使⽤线性回归模型真实权重w = [2, -3.4]⊤和偏差b = 4.2,以及⼀个随机噪声项ϵ来⽣成标签:
y=Xw+b+ϵy = Xw + b + ϵy=Xw+b+ϵ
实际上这个公式可以写成这样以便理解:
y=Xy = Xy=X1www 1+X+X+X2www2+b+ϵ+ b + ϵ+b+ϵ
其中噪声项ϵ服从均值为0、标准差为0.01的正态分布。噪声代表了数据集中⽆意义的⼲扰。下⾯,让我们⽣成数据集。
num_inputs = 2 #输入个数(特征数)
num_example = 1000 #样本数
true_w = [2, -3.4] #设置真实权重w
true_b = 4.2 #设置真实偏差b
#features为数据集,即生成了行数为1000,列数为2的服从均值为0、标准差为1的正态分布的随机数据
features = nd.random.normal(scale=1, shape=(num_example, num_inputs))
#labels是正确的符合y = Xw + b + ϵ的y值矩阵
labels = true_w[0] * features[:, 0] + true_w[1] * features[:, 1] + true_b
#我们为这个y值加上一个很小的噪声项ϵ,生成了有些许偏差的标签y值矩阵
labels += nd.random.normal(scale=0.01, shape=labels.shape)
为labels加上噪声项ϵ的意义
从代码中我们可以看到,最开始的lables是通过真实的权重www和偏差bbb,利用y=Xw+by = Xw + by=Xw+b求出来的,为的是让lables跟www具有线性关系。而我们线性回归模型的任务是求出根据labels去求出www和bbb,现实中的数据不可能完美地符合线性回归,所及加上噪声项去模拟有些许偏差的测试数据。
注意
features的每⼀⾏是⼀个⻓度为2的向量,而labels的每⼀⾏是⼀个⻓度为1的向量(标量)
在训练模型的时候,我们需要遍历数据集并不断读取小批量数据样本。这⾥我们定义⼀个函数:
它每次返回batch_size(批量⼤小)个随机样本的特征和标签。
def data_iter(batch_size, features, labels):
num_example = len(features)
indices = list(range(num_example))
# 样本的读取顺序是随机的,这里之后indices就变成了一个包含打乱了顺序的0-999数字的集合
random.shuffle(indices)
for i in range(0, num_example, batch_size):
j = nd.array(indices[i:min(i + batch_size, num_example)])
yield features.take(j), labels.take(j) # take函数根据索引返回对应元素
batch_size = 10;#先在这里定义好批量大小为10
每个批量的特征形状为(10, 2),分别对应批量⼤小和输⼊个数;标签形状为批量⼤小。
3.初始化模型参数我们将权重初始化成均值为0、标准差为0.01的正态随机数,偏差则初始化成0。
# 初始化模型参数
w = nd.random.normal(scale=0.01, shape=(num_inputs, 1))
b = nd.zeros(shape=(1,))
注意这个www和bbb与上面的定义的true_w和true_b的差别
这里的是我们初始化的w和b,即我们在不知道真实www和bbb时随机设置的初始值,在后面的训练中www和bbb会不断接近真实的true_w和true_b。
之后的模型训练中,需要对这些参数求梯度来迭代参数的值,因此我们需要创建它们的梯度。
#创建w和b的梯度
w.attach_grad()
b.attach_grad()
4.定义模型
下⾯是线性回归的⽮量计算表达式的实现。我们使⽤dot函数做矩阵乘法。
def linreg(X, w, b):
return nd.dot(X, w) + b
理解这个函数
这个函数需要X,w,b三个参数,X即我们的特征矩阵features,w和b是我们自己定义的参数(而不是true_w和true_b),这样做了dot矩阵乘法后,return出来的就是经过XXX1www 1+X+X+X2www2+b+b+b计算得到的yyy值矩阵
我们定义一个损失函数,用来计算我们的线性回归模型计算出来的yyy值和真实的yyy值(含有噪声项)之间的差距(即损失),后面需要对这个损失求梯度。
# 定义损失函数
#y_hat是预测值,y是真实值
def squared_loss(y_hat, y):
return (y_hat - y.reshape(y_hat.shape)) ** 2 / 2
上面提到,真实值y(即labels)是一个标量,而预测值y_hat是由矩阵经过计算得到的,是一个向量,两者无法进行数学运算,所以我们需要把真实值y变形成预测值y_hat的形状,就相当于把标量变成了向量。函数返回的结果也将和y_hat的形状相同(即也是向量)。
6.定义优化算法以下的sgd函数实现了小批量随机梯度下降算法。它通过不断迭代模型参数来优化损失函数。这⾥⾃动求梯度模块计算得来的梯度是⼀个批量样本的梯度和。我们将它除以批量⼤小来得到平均值。
def sgd(params, lr, batch_size):
for param in params:
param[:] = param - lr * param.grad / batch_size
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 #将模型函数重命名为net
loss = squared_loss #将损失函数重命名为loss
for epoch in range(num_epochs):
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()))
训练过程部分打印信息如下:
epoch1,loss15.373573
epoch1,loss14.425648
epoch1,loss13.684246
......
epoch2,loss0.007572
epoch2,loss0.007226
epoch2,loss0.006662
......
epoch3,loss0.000049
epoch3,loss0.000049
epoch3,loss0.000049
可以看到,在训练过程中,损失一直在减少。打印出学来的参数w和b与真实参数来比较它们之间的差距(因为有噪声项的干扰,两者必然不相等):
代码:
print(true_b, b)
print(true_w, w)
结果:
[2, -3.4] #真实参数w
[[ 1.9999706]
[-3.3994975]] #训练之后的模型参数w
4.2 #真实参数b
[4.19931] #训练之后的模型参数b
可以看到两者十分的接近!
8.完整代码# ==========本节将介绍如何只利⽤NDArray和autograd来实现⼀个线性回归的训练。=============
from IPython import display
from mxnet import autograd, nd
import random
# 生成数据集
num_inputs = 2
num_example = 1000
true_w = [2, -3.4]
true_b = 4.2
features = nd.random.normal(scale=1, shape=(num_example, 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)
# 读取数据
# 在训练模型的时候,我们需要遍历数据集并不断读取小批量数据样本。
# 这⾥我们定义⼀个函数:它每次返回batch_size(批量⼤小)个随机样本的特征和标签。
def data_iter(batch_size, features, labels):
num_example = len(features)
indices = list(range(num_example))
random.shuffle(indices) # 样本的读取顺序是随机的
for i in range(0, num_example, batch_size):
j = nd.array(indices[i:min(i + batch_size, num_example)])
yield features.take(j), labels.take(j) # take函数根据索引返回对应元素
batch_size = 10;
# 初始化模型参数
w = nd.random.normal(scale=0.01, shape=(num_inputs, 1))
b = nd.zeros(shape=(1,))
w.attach_grad()
b.attach_grad()
# 定义模型
# 下⾯是线性回归的⽮量计算表达式的实现。我们使⽤dot函数做矩阵乘法。
def linreg(X, w, b):
return nd.dot(X, w) + b
# 定义损失函数
def squared_loss(y_hat, y):
return (y_hat - y.reshape(y_hat.shape)) ** 2 / 2
# 定义优化算法
# 以下的sgd函数实现了上⼀节中介绍的小批量随机梯度下降算法。
# 它通过不断迭代模型参数来优化损失函数。
# 这⾥⾃动求梯度模块计算得来的梯度是⼀个批量样本的梯度和。我们将它除以批量⼤小来得到平均值。
def sgd(params, lr, batch_size):
for param in params:
param[:] = param - lr * param.grad / batch_size
# 训练模型
lr = 0.03
num_epochs = 3
net = linreg
loss = squared_loss
# 训练模型⼀共需要num_epochs个迭代周期
# 在每⼀个迭代周期中,会使⽤训练数据集中所有样本⼀次(假设样本数能够被批量⼤⼩整除)。
# X和y分别是⼩批量样本的特征和标签
for epoch in range(num_epochs):
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()))
print(true_b, b)
print(true_w, w)