欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2022-06-06 08:41:29
...

线性回归的从零实现

尽管强⼤的深度学习框架可以减少⼤量重复性⼯作,但若过于依赖它提供的便利,会导致我们很难深⼊理解深度学习是如何⼯作的。因此,本节将介绍如何只利⽤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=Xy = X1ww 1+X+X2ww2+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是通过真实的权重ww和偏差bb,利用y=Xw+by = Xw + b求出来的,为的是让lables跟ww具有线性关系。而我们线性回归模型的任务是求出根据labels去求出wwbb,现实中的数据不可能完美地符合线性回归,所及加上噪声项去模拟有些许偏差的测试数据。

注意
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,))

注意这个wwbb与上面的定义的true_w和true_b的差别
这里的是我们初始化的w和b,即我们在不知道真实wwbb时随机设置的初始值,在后面的训练中wwbb会不断接近真实的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出来的就是经过XX1ww 1+X+X2ww2+b+b计算得到的yy值矩阵

5.定义损失函数

我们定义一个损失函数,用来计算我们的线性回归模型计算出来的yy值和真实的yy值(含有噪声项)之间的差距(即损失),后面需要对这个损失求梯度。

# 定义损失函数
#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)

结果:

<NDArray 1 @cpu(0)>
[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)