通过代码从零实现线性回归,包括数据流水线、模型、损失函数和小批量随机梯度下降优化器。 虽然现代的深度学习框架几乎可以自动化地进行所有这些工作,但从零开始实现可以确保我们真正知道自己在做什么。 同时,了解更细致的工作原理将方便我们自定义模型、自定义层或自定义损失函数。
💻 参考资料:李沐《动手学深度学习-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}
X∈R1000×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∣ηi∈B∑∂(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}
wb←w−∣B∣ηi∈B∑∂wl(i)(w,b)=w−∣B∣ηi∈B∑x(i)(w⊤x(i)+b−y(i)),←b−∣B∣ηi∈B∑∂bl(i)(w,b)=b−∣B∣ηi∈B∑(w⊤x(i)+b−y(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)∣B∣1∑i∈Bl(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 线性回归的简洁实现