|
以下内容为结合李沐老师的课程和教材补充的学习笔记,以及对课后操练的一些思考,自留回顾,也供同学之人交流参考。
本节课程地址:线性回归的从零开始实现_哔哩哔哩_bilibili
本节教材地址:3.2. 线性回归的从零开始实现 — 动手学深度学习 2.0.0 documentation (d2l.ai)
本节开源代码:...>d2l-zh>pytorch>chapter_linear-networks>linear-regression-scratch
<hr/>线性回归的从零开始实现
在了解线性回归的关键思想之后,我们可以开始通过代码来动手实现线性回归了。 在这一节中,(我们将从零开始实现整个方式, 包罗数据流水线、模型、损掉函数和小批量随机梯度下降优化器)。 虽然现代的深度学习框架几乎可以自动化地进行所有这些工作,但从零开始实现可以确保我们真正知道本身在做什么。 同时,了解更细致的工作道理将便利我们自定义模型、自定义层或自定义损掉函数。 在这一节中,我们将只使用张量和自动求导。 在之后的章节中,我们会充实操作深度学习框架的优势,介绍更简洁的实现方式。
%matplotlib inline
import random
import torch
from d2l import torch as d2l生成数据集
为了简单起见,我们将[按照带有噪声的线性模型构造一个人造数据集。] 我们的任务是使用这个有限样本的数据集来恢复这个模型的参数。 我们将使用低维数据,这样可以很容易地将其可视化。 不才面的代码中,我们生成一个包含1000个样本的数据集, 每个样本包含从尺度正态分布中采样的2个特征。 我们的合成数据集是一个矩阵 \mathbf{X}\in \mathbb{R}^{1000 \times 2} 。
(**我们使用线性模型参数 \mathbf{w} = [2, -3.4]^\top、b = 4.2 和噪声项 \epsilon 生成数据集及其标签:
\mathbf{y}= \mathbf{X} \mathbf{w} + b + \mathbf\epsilon. **)
\epsilon 可以视为模型预测和标签时的潜在不雅观测误差。 在这里我们认为尺度假设成立,即 \epsilon 从命均值为0的正态分布。 为了简化问题,我们将尺度差设为0.01。 下面的代码生成合成数据集。
def synthetic_data(w, b, num_examples): #@save
”””生成y=Xw+b+噪声”””
X = torch.normal(0, 1, (num_examples, len(w)))
y = torch.matmul(X, w) + b
y += torch.normal(0, 0.01, y.shape)
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中的每一行都包含一个二维数据样本(X), labels中的每一行都包含一维标签值(一个标量)(y)]。
print('features:', features[0],'\nlabel:', labels[0])输出成果:
features: tensor([1.1331, 0.0947])
label: tensor([6.1539])
通过生成第二个特征features[:, 1]和labels的散点图, 可以直不雅观不雅察看到两者之间的线性关系。
d2l.set_figsize()
d2l.plt.scatter(features[:, 1].detach().numpy(), labels.detach().numpy(), 1);
# 使用detach().numpy()将tensor对象从计算图中分手出来,并转换为numpy数组,进行绘制
读取数据集
回想一下,训练模型时要对数据集进行遍历,每次抽取一小批量样本,并使用它们来更新我们的模型。 由于这个过程是训练机器学习算法的基础,所以有必要定义一个函数, 该函数能打乱数据集中的样本并以小批量方式获取数据。
不才面的代码中,我们[定义一个data_iter函数, 该函数接收批量大小、特征矩阵和标签向量作为输入,生成大小为batch_size的小批量]。 每个小批量包含一组特征和标签。
def data_iter(batch_size, features, labels):
num_examples = len(features)
indices = list(range(num_examples))
# 这些样本是随机读取的,没有特定的挨次
random.shuffle(indices)
for i in range(0, num_examples, batch_size):
batch_indices = torch.tensor(
indices[i: min(i + batch_size, num_examples)])
yield features[batch_indices], labels[batch_indices]凡是,我们操作GPU并行运算的优势,措置合理大小的“小批量”。 每个样本都可以并行地进行模型计算,且每个样本损掉函数的梯度也可以被并行计算。 GPU可以在措置几百个样本时,所花费的时间不比措置一个样本时多太多。
我们直不雅观感到感染一下小批量运算:读取第一个小批量数据样本并打印。 每个批量的特征维度显示批量大小和输入特征数。 同样的,批量的标签形状与batch_size相等。
batch_size = 10
for X, y in data_iter(batch_size, features, labels):
print(X, '\n', y)
break输出成果:
tensor([[-0.6966, 0.2079],
[-1.4374, -0.1049],
[-0.3717, -0.0734],
[-1.7703, 0.1110],
[ 3.1394, -1.1923],
[ 0.1055, 0.3089],
[ 1.5195, 0.5335],
[-2.1175, -0.3790],
[ 0.4449, -0.2879],
[ 1.5303, -0.2278]])
tensor([[ 2.1241],
[ 1.6791],
[ 3.7046],
[ 0.2787],
[14.5269],
[ 3.3543],
[ 5.4206],
[ 1.2500],
[ 6.0610],
[ 8.0352]])
当我们运行迭代时,我们会持续地获得分歧的小批量,直至遍历完整个数据集。 上面实现的迭代对教学来说很好,但它的执行效率很低,可能会在实际问题上陷入麻烦。 例如,它要求我们将所有数据加载到内存中,并执行大量的随机内存访谒。 在深度学习框架中实现的内置迭代器效率要高得多, 它可以措置存储在文件中的数据和数据流提供的数据。
初始化模型参数
[在我们开始用小批量随机梯度下降优化我们的模型参数之前], (我们需要先有一些参数)。 不才面的代码中,我们通过从均值为0、尺度差为0.01的正态分布中采样随机数来初始化权重, 并将偏置初始化为0。
w = torch.normal(0, 0.01, size=(2,1), requires_grad=True)
b = torch.zeros(1, requires_grad=True)在初始化参数之后,我们的任务是更新这些参数,直到这些参数足够拟合我们的数据。 每次更新都需要计算损掉函数关于模型参数的梯度。 有了这个梯度,我们就可以向减小损掉的标的目的更新每个参数。 因为手动计算梯度很枯燥而且容易犯错,所以没有人会手动计算梯度。 我们使用 2.5节 中引入的自动微分来计算梯度。
定义模型
接下来,我们必需[定义模型,将模型的输入和参数同模型的输出关联起来。] 回想一下,要计算线性模型的输出, 我们只需计算输入特征 \mathbf{X} 和模型权重 \mathbf{w} 的矩阵-向量乘法后加上偏置 b 。 注意,上面的 \mathbf{Xw} 是一个向量,而b是一个标量。 回想一下2.1.3节 中描述的广播机制: 当我们用一个向量加一个标量时,标量会被加到向量的每个分量上。
def linreg(X, w, b): #@save
”””线性回归模型”””
return torch.matmul(X, w) + b[定义损掉函数]
因为需要计算损掉函数的梯度,所以我们应该先定义损掉函数。 这里我们使用 3.1节 中描述的平方损掉函数。 在实现中,我们需要将真实值y的形状转换为和预测值y_hat的形状不异。
def squared_loss(y_hat, y): #@save
”””均方损掉”””
return (y_hat - y.reshape(y_hat.shape)) ** 2 / 2(定义优化算法)
正如我们在3.1节中讨论的,线性回归有解析解。 尽管线性回归有解析解,但本书中的其他模型却没有。 这里我们介绍小批量随机梯度下降。
在每一步中,使用从数据集中随机抽取的一个小批量,然后按照参数计算损掉的梯度。 接下来,朝着减少损掉的标的目的更新我们的参数。 下面的函数实现小批量随机梯度下降更新。 该函数接受模型参数调集、学习速率和批量大小作为输入。每 一步更新的大小由学习速率lr决定。 因为我们计算的损掉是一个批量样本的总和,所以我们用批量大小(batch_size) 来规范化步长,这样步长大小就不会取决于我们对批量大小的选择。
def sgd(params, lr, batch_size): #@save
”””小批量随机梯度下降”””
with torch.no_grad():
for param in params: # param为 w, b
param -= lr * param.grad / batch_size
param.grad.zero_()训练
此刻我们已经筹备好了模型训练所有需要的要素,可以实现主要的[训练过程]部门了。 理解这段代码至关重要,因为从事深度学习后, 不异的训练过程几乎一遍又一遍地呈现。 在每次迭代中,我们读取一小批量训练样本,并通过我们的模型来获得一组预测。 计算完损掉后,我们开始反向传布,存储每个参数的梯度。 最后,我们调用优化算法sgd来更新模型参数。
概括一下,我们将执行以下循环:
- 初始化参数
- 反复以下训练,直到完成
- 计算梯度$\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)$
- 更新参数$(\mathbf{w}, b) \leftarrow (\mathbf{w}, b) - \eta \mathbf{g}$
在每个迭代周期(epoch)中,我们使用data_iter函数遍历整个数据集, 并将训练数据集中所有样本都使用一次(假设样本数能够被批量大小整除)。 这里的迭代周期个数num_epochs和学习率lr都是超参数,分袂设为3和0.03。 设置超参数很棘手,需要通过反复试验进行调整。 我们此刻忽略这些细节,以后会在 :numref:chap_optimization中详细介绍。
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):
l = loss(net(X, w, b), y) # X和y的小批量损掉
# 因为l形状是(batch_size,1),而不是一个标量。l中的所有元素被加到一起,
# 并以此计算关于[w,b]的梯度
l.sum().backward()
sgd([w, b], lr, batch_size) # 使用参数的梯度更新参数
with torch.no_grad():
train_l = loss(net(features, w, b), labels)
print(f'epoch {epoch + 1}, loss {float(train_l.mean()):f}')输出成果:
epoch 1, loss 0.044127
epoch 2, loss 0.000184
epoch 3, loss 0.000051
因为我们使用的是本身合成的数据集,所以我们知道真正的参数是什么。 因此,我们可以通过[斗劲真实参数和通过训练学到的参数来评估训练的成功程度]。 事实上,真实参数和通过训练学到的参数确实非常接近。
print(f'w的估计误差: {true_w - w.reshape(true_w.shape)}')
print(f'b的估计误差: {true_b - b}')输出成果:
w的估计误差: tensor([ 0.0002, -0.0009], grad_fn=<SubBackward0>)
b的估计误差: tensor([-0.0001], grad_fn=<RsubBackward1>)
注意,我们不应该想当然地认为我们能够完美地求解参数。 在机器学习中,我们凡是不太关心恢复真正的参数,而更关心如何高度准确预测参数。 幸运的是,即使是在复杂的优化问题上,随机梯度下降凡是也能找到非常好的解。 此中一个原因是,在深度网络中存在许多参数组合能够实现高度精确的预测。
小结
- 我们学习了深度网络是如何实现和优化的。在这一过程中只使用张量和自动微分,不需要定义层或复杂的优化器。
- 这一节只触及到了概况常识。不才面的部门中,我们将基于方才介绍的概念描述其他模型,并学习如何更简洁地实现其他模型。
操练
1. 如果我们将权重初始化为零,会发生什么。算法仍然有效吗?
解:
在线性回归中,由于只有一层神经网络,且SGD过程中,梯度求导后成果与参数本身无关,而是取决于输入 \mathbf{X} 和 \mathbf{y} ,因此,可以将权重初始化为0,算法仍然有效(试验如下)。
但是,在多层神经网络中,如果将权重初始化为0,或者其他统一的常量,会导致后面迭代的权重更新不异,而且神经网络中的激活单元的值不异,输出的梯度也相等,导致对称性问题,无法进行独立学习,找到最优解。
w = torch.zeros((2,1), requires_grad=True)
b = torch.zeros(1, requires_grad=True)
for epoch in range(num_epochs):
for X, y in data_iter(batch_size, features, labels):
l = loss(net(X, w, b), y) # X和y的小批量损掉
# 因为l形状是(batch_size,1),而不是一个标量。l中的所有元素被加到一起,
# 并以此计算关于[w,b]的梯度
l.sum().backward()
sgd([w, b], lr, batch_size) # 使用参数的梯度更新参数
with torch.no_grad():
train_l = loss(net(features, w, b), labels)
print(f'epoch {epoch + 1}, loss {float(train_l.mean()):f}')输出成果:
epoch 1, loss 0.044151
epoch 2, loss 0.000182
epoch 3, loss 0.000052
2. 假设试图为电压和电流的关系成立一个模型。自动微分可以用来学习模型的参数吗?
解:
将电压和电流的关系成立模型为U=Iw+b,转化为线性回归问题,可用自动微分学习模型的参数并更新,逐步优化模型使得损掉函数最小化,并使得模型能够更好地拟合电压和电流之间的关系。
3. 能基于普朗克定律使用光谱能量密度来确定物体的温度吗?
解:
可以基于普朗克定律,使用光谱能量密度来确定物体的温度。如下:
按照普朗克定律,黑体辐射的能谱密度可以暗示为: B(λ, T) = (2hc^2 / λ^5) / (e^(hc/λkT) - 1) 此中,B(λ, T)是波长为λ时温度为T的黑体辐射的能谱密度,h是普朗克常数,c是光速,k是玻尔兹曼常数。
按照此定律,当给定了物体的光谱能量密度分布(即分歧波长上的能谱密度值),我们可以反推出物体的温度。具体的法式如下:
1)从物体获取光谱数据,即记录分歧波长上的能谱密度值。
2)将能谱密度值带入普朗克定律的表达式中。
3)操作SGD,将测量到的能谱密度与普朗克定律的表达式中得到的能谱密度进行斗劲。
4)通过迭代,找到与实测数据最匹配的温度值,这个温度值就是物体的温度。
4. 计算二阶导数时可能会遇到什么问题?这些问题可以如何解决?
解:
二阶导数包含了更多关于损掉函数曲率的信息,因此在某些情况下,计算二阶导数可能有助于更快地收敛和更准确的更新。然而,由于计算复杂度较高,凡是在实际应用中很少使用。
以下是计算二阶导数时可能会遇到的问题,以及可能的解决方式:
1)计算复杂度高: 计算Hessian矩阵需要更多的计算资源和时间,尤其在大规模数据和复杂模型上。
解决方式: 凡是可以使用近似方式来估计二阶导数,例如L-BFGS(Limited-memory Broyden-Fletcher-Goldfarb-Shanno)等优化算法。这些方式在必然程度上降低了计算成本,同时仍能提供较好的优化效果。
2)存储需求大: Hessian矩阵的存储需求随着参数数量的增加而增加,可能导致内存不足的问题。
解决方式: 使用一些高效的矩阵近似方式,如块对角近似(block-diagonal approximation)或采样Hessian近似,来减少存储需求。
3)数值不不变性: 在计算Hessian矩阵时,可能会遇到数值不不变性,导致数值误差累积,影响优化成果。
解决方式: 使用数值不变的计算方式,例如通过添加小的正则化项来避免矩阵的奇异性。此外,选择合适的优化算法和学习率调剂也可以辅佐不变优化过程。
4)局部极小值和鞍点: 在高维空间中,存在许多局部极小值和鞍点,这可能导致Hessian矩阵的谱值较小,使得计算二阶导数的成果不不变。
解决方式: 使用正则化技术、随机性优化方式(如随机梯度牛顿法)或基于自适应学习率的算法,可以辅佐逃离局部极小值和鞍点。
总之,虽然计算二阶导数在优化中具有必然的潜在优势,但在实际应用中,由于上述问题和计算成本,往往更常使用一阶优化方式(如SGD、Adam等)及其变种。选择优化方式时,需要按照具体问题的特点来权衡二阶信息带来的优势和计算成本。
5. 为什么在squared_loss函数中需要使用reshape函数?
解:
因为y_hat和y的元素个数不异,但shape不必然不异(虽然在本节中二者shape一致),为了保证计算时不犯错,故使用reshape函数将二者的shape统一。
6. 测验考试使用分歧的学习率,不雅察看损掉函数值下降的快慢。
解:
学习率越小,损掉函数值下降越慢,反之亦然;
但当学习率过高时,损掉函数值将呈现inf和nan。试验如下:
w = torch.normal(0, 0.01, size=(2,1), requires_grad=True)
b = torch.zeros(1, requires_grad=True)
lr = 0.001
num_epochs = 3
net = linreg
loss = squared_loss
for epoch in range(num_epochs):
for X, y in data_iter(batch_size, features, labels):
l = loss(net(X, w, b), y) # X和y的小批量损掉
# 因为l形状是(batch_size,1),而不是一个标量。l中的所有元素被加到一起,
# 并以此计算关于[w,b]的梯度
l.sum().backward()
sgd([w, b], lr, batch_size) # 使用参数的梯度更新参数
with torch.no_grad():
train_l = loss(net(features, w, b), labels)
print(f'epoch {epoch + 1}, loss {float(train_l.mean()):f}')输出成果:
epoch 1, loss 13.210416
epoch 2, loss 10.882115
epoch 3, loss 8.964668
lr = 3
num_epochs = 3
net = linreg
loss = squared_loss
for epoch in range(num_epochs):
for X, y in data_iter(batch_size, features, labels):
l = loss(net(X, w, b), y) # X和y的小批量损掉
# 因为l形状是(batch_size,1),而不是一个标量。l中的所有元素被加到一起,
# 并以此计算关于[w,b]的梯度
l.sum().backward()
sgd([w, b], lr, batch_size) # 使用参数的梯度更新参数
with torch.no_grad():
train_l = loss(net(features, w, b), labels)
print(f'epoch {epoch + 1}, loss {float(train_l.mean()):f}')输出成果:
epoch 1, loss inf
epoch 2, loss nan
epoch 3, loss nan
6. 如果样本个数不能被批量大小整除,data_iter函数的行为会有什么变化?
解:
当num_examples不能被batch_size整除,data_iter函数的行为会有以下变化:
1)最后一个批次的样本数量会小于batch_size:在for循环中,当i递增到最后一个不完整的批次时,min(i+batch_size, num_examples)会返回一个小于batch_size的值,这意味着最后一个批次的样本数量会少于batch_size。
2)最后一个批次中的样本挨次将不必然是随机的:由于样本在indices列表中是随机摆列的,而最后一个批次的样本数量小于batch_size,因此在取最后一个批次时,实际上是从indices列表末尾开始继续取样本,直达到到indices列表的末尾。因此,最后一个批次中的样本挨次将不再随机,而是按照indices列表的挨次。
随机化样本的目的凡是是为了打乱样本的挨次,使得模型在训练过程中能够接触到各种分歧类型的样本。但由于最后一个不完整批次的样本挨次不再随机,可能会引入一些偏差。因此,在设计实际应用中的data_iter函数时,可以考虑在最后一个不完整批次中从头进行随机化操作,以减小这种偏差的影响。
试验如下:
def data_iter(batch_size, features, labels):
num_examples = int(len(features)/100)
indices = list(range(num_examples))
# 这些样本是随机读取的,没有特定的挨次
random.shuffle(indices)
for i in range(0, num_examples, batch_size):
batch_indices = torch.tensor(
indices[i: min(i + batch_size, num_examples)])
yield features[batch_indices], labels[batch_indices]
batch_size = 7
for X, y in data_iter(batch_size, features, labels):
print(X, '\n', y)输出成果:
tensor([[ 1.0903, -0.0947],
[ 0.1203, 0.9302],
[-0.0843, -1.1150],
[-0.2253, 0.2689],
[-1.3705, 1.4262],
[-2.5419, 2.3738],
[-1.9008, 1.2435]])
tensor([[ 6.7004],
[ 1.2781],
[ 7.8223],
[ 2.8348],
[-3.3952],
[-8.9684],
[-3.8481]])
tensor([[-0.3739, 0.0578],
[ 1.1188, 2.2842],
[ 1.1331, 0.0947]])
tensor([[ 3.2707],
[-1.3230],
[ 6.1539]]) |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|