《动手学深度学习》之线性回归的从零实现(含个人理解)

Lark ·
更新时间:2024-11-13
· 754 次阅读

线性回归的从零实现

尽管强⼤的深度学习框架可以减少⼤量重复性⼯作,但若过于依赖它提供的便利,会导致我们很难深⼊理解深度学习是如何⼯作的。因此,本节将介绍如何只利⽤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的向量(标量)

2.读取数据

在训练模型的时候,我们需要遍历数据集并不断读取小批量数据样本。这⾥我们定义⼀个函数:
它每次返回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值矩阵

5.定义损失函数

我们定义一个损失函数,用来计算我们的线性回归模型计算出来的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)
作者:鸿鸿只爱清清



动手学 学习 回归 深度学习 线性 线性回归

需要 登录 后方可回复, 如果你还没有账号请 注册新账号