3.2 线性回归的从零开始实现

通过代码从零实现线性回归,包括数据流水线、模型、损失函数和小批量随机梯度下降优化器。 虽然现代的深度学习框架几乎可以自动化地进行所有这些工作,但从零开始实现可以确保我们真正知道自己在做什么。 同时,了解更细致的工作原理将方便我们自定义模型、自定义层或自定义损失函数。

💻 参考资料:李沐《动手学深度学习-Pytorch版》📢ch3线性神经网络
🎈 开源地址:动手学深度学习
🔊 链接至上一节:3.1 线性回归
🎀 此篇仅仅学习记录,更详细的内容可参考开源的书和代码以及b站上李沐老师的视频动手学深度学习在线课程

1. 线性回归的从零开始实现

1.0 导入Python包

%matplotlib inline
import random
import torch
from d2l import torch as d2l

1.1 生成数据集

根据带有噪声的线性模型构造一个人造数据集。任务是使用这个有限样本的数据集来恢复这个模型的参数。先生成一个包含1000个样本的数据集,每个样本包含从标准正态分布中采样的2个特征。合成数据集是一个矩阵 X ∈ R 1000 × 2 \mathbf{X}\in \mathbb{R}^{1000 \times 2} XR1000×2。使用线性模型参数 w = [ 2 , − 3.4 ] ⊤ \mathbf{w} = [2, -3.4]^\top w=[2,3.4] b = 4.2 b = 4.2 b=4.2 和噪声项 ϵ \epsilon ϵ生成数据集及其标签:
y = X w + b + ϵ . \mathbf{y}= \mathbf{X} \mathbf{w} + b + \mathbf\epsilon. y=Xw+b+ϵ. ϵ \epsilon ϵ可以视为模型预测和标签时的潜在观测误差。在这里认为标准假设成立,即 ϵ \epsilon ϵ服从均值为0,标准差为0.01的正态分布。

def synthetic_data(w,b,num_examples):  # num_examplesz:要生成的数据集的样本数量
    """生成y=Xw+b+噪声"""
    # 使用torch.normal方法生成一个形状为(num_examples, len(w))的张量X,其中每个元素都是从标准正态分布中随机采样得到的。
    X = torch.normal(0,1,(num_examples,len(w)))
    # 生成输出标签y,形状为(num_examples, 1),通过矩阵乘法计算
    y = torch.matmul(X,w)+b
    # 函数向输出标签y添加一些噪声,使得标签y不完全等于输入特征X与权重w的线性组合
    y += torch.normal(0,0.01,y.shape)
    # 最终返回生成的数据集,包括输入特征X和输出标签y。
    # 其中y.reshape((-1, 1))用于将y的形状从(num_examples,)转换为(num_examples, 1)。
    return X,y.reshape((-1,1))
true_w = torch.tensor([2, -3.4])
true_b = 4.2
features, labels = synthetic_data(true_w, true_b, 1000)
# features中的每一行都包含一个二维数据样本,labels中的每一行都包含一维标签值(一个标量)
print('features:',features[0],'\nlabel:',labels[0])
features: tensor([0.8709, 2.1011]) 
label: tensor([-1.2160])
d2l.set_figsize() # 设置绘图的尺寸
# d2l.plt.scatter()函数用于绘制散点图
d2l.plt.scatter(features[:, (1)].detach().numpy(), labels.detach().numpy(), 1);
# features[:, (1)].detach().numpy()将数据集features的第二个特征(列索引为1)的特征提取出来,并将其转换为Numpy数组
# labels.detach().numpy() 将标签数据转换为Numpy数组
# 这两个数组分别作为x和y参数传递给d2l.plt.scatter绘制散点图

在这里插入图片描述

1.2 读取数据集

训练模型时需要对数据集进行遍历,每次抽取一小批量样本,并使用它们来更新我们的模型。因此需要定义一个函数,该函数能打乱数据集中的样本并以小批量方式获取数据。在下面的代码中,定义一个data_iter函数,该函数接收批量大小、特征矩阵和标签向量作为输入,生成大小为batch_size的小批量。每个小批量包含一组特征和标签。

# 定义了一个迭代器函数data_iter,用于对数据集进行批量读取
# 该函数接收批量大小,特征矩阵和标签向量作为输入,生成大小为batch_size的小批量。每个小批量包含一组特征和标签
def data_iter(bath_size,features,labels):
    # 首先获取数据集大小num_examples和一个包含所有数据集索引的列表indices。
    num_examples = len(features)  # 数据集大小
    indices = list(range(num_examples))  # 生成数据集索引
    # 使用Python内置的random.shuffle()函数随机打乱了索引列表。
    random.shuffle(indices)
    
    # 函数使用for循环迭代整个数据集,每次读取batch_size个样本
    # 在每次迭代中,函数生成当前批次的索引batch_indices,并使用torch.tensor()函数将其转换为张量。
    for i in range(0,num_examples,batch_size):
        batch_indices = torch.tensor(
        indices[i:min(i+batch_size,num_examples)])  # min(i+batch_size, num_examples)是为了确保当前批次不会超过数据集的大小。
       
        # 使用Python的yield关键字将当前批次的输入特征和输出标签作为一个元组产生
        yield features[batch_indices],labels[batch_indices]
        # 使用yield而不是return,是因为函数需要产生多个批次的数据,并且每个批次之间可能需要进行一些计算。
        # 使用yield可以使函数保持状态,并在需要时继续执行。
# 读取第一个小批量数据样本并打印
# 每个批量的特征维度显示批量大小和输入特征数,批量的标签形状与batch_size相等
batch_size = 10
for X,y in data_iter(batch_size,features,labels):
    print(X, '\n', y)
    break
tensor([[ 0.3019, -1.5202],
        [ 0.4525,  1.0048],
        [ 0.4775,  0.1311],
        [-1.1004, -0.0171],
        [-1.1399, -0.7601],
        [-0.3137,  1.9629],
        [ 0.5215,  0.9612],
        [-0.2487,  1.0916],
        [ 0.8128, -2.7877],
        [-1.2752,  1.2781]]) 
 tensor([[ 9.9701e+00],
        [ 1.6882e+00],
        [ 4.7130e+00],
        [ 2.0474e+00],
        [ 4.5139e+00],
        [-3.0938e+00],
        [ 1.9869e+00],
        [ 3.2526e-03],
        [ 1.5288e+01],
        [-2.6976e+00]])

1.3 初始化模型参数

在开始用小批量随机梯度下降优化模型参数之前,(需要先有一些参数)。在下面的代码中,通过从均值为0、标准差为0.01的正态分布中采样随机数来初始化权重,并将偏置初始化为0。

w = torch.normal(0, 0.01, size=(2,1), requires_grad=True)
b = torch.zeros(1, requires_grad=True)
# 在PyTorch中,requires_grad=True是一个张量(tensor)属性,指定张量是否需要计算梯度(gradient)。
# 当一个张量的requires_grad属性被设置为True时,PyTorch会追踪所有对该张量的操作,并且可以自动计算其梯度,这是实现自动微分(autograd)的重要机制。
# 当进行反向传播(backpropagation)计算梯度时,所有具有requires_grad=True属性的张量,其梯度都会被计算并存储在.grad属性中。
w,b
(tensor([[-0.0024],
         [-0.0070]], requires_grad=True),
 tensor([0.], requires_grad=True))

在初始化参数之后,我们的任务是更新这些参数,直到这些参数足以拟合我们的数据每次更新都需要计算损失函数关于模型参数的梯度,有了这个梯度,我们就可以向减小损失的方向更新每个参数。

1.4 定义模型

接下来,必须定义模型,将模型的输入和参数同模型的输出关联起来。要计算线性模型的输出,我们只需计算输入特征 X \mathbf{X} X和模型权重 w \mathbf{w} w的矩阵-向量乘法后加上偏置 b b b X w \mathbf{Xw} Xw是一个向量,而 b b b是一个标量。此处还会用到广播机制。

def linreg(X, w, b):
    """线性回归模型"""
    return torch.matmul(X, w) + b

1.5 定义损失函数

因为需要计算损失函数的梯度,所以需要先定义损失函数。使用平方损失函数

def squared_loss(y_hat, y):
    """均方损失"""
    return (y_hat - y.reshape(y_hat.shape))**2/2
    # 将真实值y的形状转换为和预测值y_hat的形状相同

1.6 定义优化算法

在每一步中,使用从数据集中随机抽取的一个小批量,然后根据参数计算损失的梯度。接下来,朝着减少损失的方向更新我们的参数。下面的函数实现小批量随机梯度下降更新。该函数接受模型参数集合学习速率批量大小作为输入。每一步更新的大小由学习速率lr决定。因为计算的损失是一个批量样本的总和,所以用批量大小(batch_size)来规范化步长,这样步长大小就不会取决于我们对批量大小的选择。
( w , b ) ← ( w , b ) − η ∣ B ∣ ∑ i ∈ B ∂ ( w , b ) l ( i ) ( w , b ) . (\mathbf{w},b) \leftarrow (\mathbf{w},b) - \frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} \partial_{(\mathbf{w},b)} l^{(i)}(\mathbf{w},b). (w,b)(w,b)BηiB(w,b)l(i)(w,b).

算法的步骤如下:
(1)初始化模型参数的值,如随机初始化;
(2)从数据集中随机抽取小批量样本且在负梯度的方向上更新参数,并不断迭代这一步骤。
w ← w − η ∣ B ∣ ∑ i ∈ B ∂ w l ( i ) ( w , b ) = w − η ∣ B ∣ ∑ i ∈ B x ( i ) ( w ⊤ x ( i ) + b − y ( i ) ) , b ← b − η ∣ B ∣ ∑ i ∈ B ∂ b l ( i ) ( w , b ) = b − η ∣ B ∣ ∑ i ∈ B ( w ⊤ x ( i ) + b − y ( i ) ) . \begin{aligned} \mathbf{w} &\leftarrow \mathbf{w} - \frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} \partial_{\mathbf{w}} l^{(i)}(\mathbf{w}, b) = \mathbf{w} - \frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} \mathbf{x}^{(i)} \left(\mathbf{w}^\top \mathbf{x}^{(i)} + b - y^{(i)}\right),\\ b &\leftarrow b - \frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} \partial_b l^{(i)}(\mathbf{w}, b) = b - \frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} \left(\mathbf{w}^\top \mathbf{x}^{(i)} + b - y^{(i)}\right). \end{aligned} wbwBηiBwl(i)(w,b)=wBηiBx(i)(wx(i)+by(i)),bBηiBbl(i)(w,b)=bBηiB(wx(i)+by(i)).

# 定义一个使用小批量随机梯度下降(Stochastic Gradient Descent, SGD)优化器更新模型参数的函数sgd。函数的输入包括:
#     params:一个模型的参数列表,每个参数都是一个张量(tensor)对象,且需要计算梯度。
#     lr:学习率(learning rate),控制每次参数更新的步长。
#     batch_size:每个小批量数据的大小。

def sgd(params, lr, batch_size):
    """小批量随机梯度下降"""
    # 使用torch.no_grad()上下文管理器,关闭梯度计算,避免在更新参数时产生不必要的计算和内存消耗。
    with torch.no_grad():
        for param in params:
            # (1) 用当前参数的梯度值除以batch_size,得到梯度的平均值。
            # (2) 用平均梯度乘以学习率lr,得到本次参数更新的步长。
            # (3) 将参数值减去步长乘以平均梯度,完成一次参数更新。
            # (4) 将参数的梯度值清零,以便下一轮计算。
            param -= lr * param.grad / batch_size
            param.grad.zero_()

1.7 训练

现在已经准备好了模型训练所有需要的要素,可以实现主要的训练过程部分。在每次迭代中,先读取一小批量训练样本,并通过模型来获得一组预测。计算完损失后,开始反向传播,存储每个参数的梯度。最后,调用优化算法sgd来更新模型参数。概括一下,将执行以下循环:

  • 初始化参数
  • 重复以下训练,直到完成
    • 计算梯度 g ← ∂ ( w , b ) 1 ∣ B ∣ ∑ i ∈ B l ( x ( i ) , y ( i ) , w , b ) \mathbf{g} \leftarrow \partial_{(\mathbf{w},b)} \frac{1}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} l(\mathbf{x}^{(i)}, y^{(i)}, \mathbf{w}, b) g(w,b)B1iBl(x(i),y(i),w,b)
    • 更新参数 ( w , b ) ← ( w , b ) − η g (\mathbf{w}, b) \leftarrow (\mathbf{w}, b) - \eta \mathbf{g} (w,b)(w,b)ηg

在每个迭代周期(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):
    # 内层循环用于遍历每个小批量数据。
    for X, y in data_iter(batch_size, features, labels):
        # 将特征 X 和参数w,b分别传入模型 net中,得到模型的预测值。
        # 将预测值和真实标签 y 传入损失函数 loss 中,计算当前小批量数据的损失 l。   
        l = loss(net(X, w, b), y)
        # 调用 l.sum().backward() 计算 l 的梯度,并自动计算参数 w 和 b 的梯度。
        l.sum().backward()
        # 调用 sgd([w, b], lr, batch_size) 使用小批量随机梯度下降算法更新参数 w 和 b。
        sgd([w, b], lr, batch_size)
    # 循环结束后,使用 with torch.no_grad() 上下文管理器关闭梯度计算,计算整个训练集的损失 train_l,并输出当前轮数和损失值
    with torch.no_grad():
        train_l = loss(net(features, w, b), labels)
        print(f'epoch{epoch + 1}, loss{float(train_l.mean()):f}')
epoch1, loss0.037077
epoch2, loss0.000131
epoch3, loss0.000048
# 通过比较真实参数和通过训练学习的参数来评估训练的成功程度
print(f'w的估计误差:{true_w - w.reshape(true_w.shape)}')
print(f'b的估计误差:{true_b - b}')
w的估计误差:tensor([-1.2970e-04,  1.1683e-05], grad_fn=<SubBackward0>)
b的估计误差:tensor([0.0002], grad_fn=<RsubBackward1>)

2. 引用

引用原书:

@book{zhang2019dive,
    title={Dive into Deep Learning},
    author={Aston Zhang and Zachary C. Lipton and Mu Li and Alexander J. Smola},
    note={\url{http://www.d2l.ai}},
    year={2020}
}

3. 线性回归的简洁实现

🚩🚩🚩链接至下一节:3.3 线性回归的简洁实现

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值