从零开始的线性回归实现
引用翻译:《动手学深度学习》
与另一篇添加链接描述实现线性回归——【torch学习笔记】
相对应,本篇更偏向于各函数的底层实现。
在这一节中,以及接下来的类似章节中,将实现线性回归的所有部分。数据管道、模型、损失函数,和梯度下降优化器,从头开始。
今天的深度学习框架可以自动完成几乎所有这些工作。但是,如果你从来没有学会从头开始实现这些东西。那么你可能永远不会真正理解模型的工作原理。此外,当需要定制模型的时候。定义我们自己的层、损失函数等等。知道事情在引擎盖下是如何工作的将会很有用。
因此,我们首先描述如何实现线性回归,仅依靠 "torch.Tensor "和 "autograd "包中的原语,在紧接着的章节中,我们将介绍紧凑的实现,使用torch的所有功能和口哨。,但这是我们深入研究细节的地方。
首先,我们导入运行本节实验所需的包:我们将使用matplotlib
进行绘图,将其设置为嵌入GUI中。
%matplotlib inline
from IPython import display
from matplotlib import pyplot as plt
import torch
import random
一、加载数据集
在这个演示中,我们将构建一个简单的人工数据集,这样我们就可以很容易地将数据可视化,并将真实模式与所学参数进行比较。我们将设定训练集中的例子数量为1000,特征(或协变量)的数量为2。在这个例子中,我们将通过从高斯分布中对每个数据点𝐱𝑖进行采样来合成我们的数据。
此外,为了确保我们的算法有效,我们将假设线性假设成立,真实的基础参数𝐰=[2,-3.4]⊤和𝑏=4.2。因此,我们的合成标签将根据以下线性模型给出,其中包括一个噪声项𝜖,以考虑到特征和标签的测量误差。
𝐲=𝐗𝐰+𝑏+𝜖
根据标准假设,我们选择一个噪声项𝜖,它服从均值为0的正态分布,在本例中,我们将其标准差设为0.01。下面的代码生成了我们的合成数据集。
num_inputs = 2
num_examples = 1000
true_w = torch.tensor([2, -3.4])
true_b = 4.2
features = torch.zeros(size=(num_examples, num_inputs)).normal_()
labels = torch.matmul(features, true_w) + true_b
labels += torch.zeros(size=labels.shape).normal_(std=0.01)
请注意,特征中的每一行由一个二维的数据点组成,标签中的每一行由一个一维的目标值(标量)组成。
features[0], labels[0]
(tensor([-0.8880, -1.7131]), tensor(8.2496))
通过使用第二个特征[:, 1]和标签生成散点图,我们可以清楚地观察到两者之间的线性相关关系。
def use_svg_display():
# Display in vector graphics
display.set_matplotlib_formats('svg')
def set_figsize(figsize=(3.5, 2.5)):
use_svg_display()
# Set the size of the graph to be plotted
plt.rcParams['figure.figsize'] = figsize
set_figsize()
plt.figure(figsize=(10, 6))
plt.scatter(features[:, 1].numpy(), labels.numpy(), 1);
绘图函数plt以及use_svg_display和set_figsize函数都定义在d2l包中。现在你知道如何自己制作绘图了,我们将直接调用d2l.plt来进行未来的绘图。为了打印矢量图并设置其大小,我们只需要在绘图前调用d2l.set_figsize(),因为plt是d2l包中的一个全局变量。
二、读取数据
回顾一下,训练模型包括对数据集进行多次处理,一次抓取一个小批量的例子,并使用它们来更新我们的模型。由于这个过程是训练机器学习算法的基础,我们需要一个工具来洗刷数据和访问小批量的数据。
在下面的代码中,我们定义了一个data_iter函数来演示这个功能的一个可能的实现。该函数接收一个批次大小、一个包含特征的设计矩阵和一个标签向量,产生大小为batch_size的迷你批次,每个批次由一个特征和标签的元组组成。
# 这个功能已经被保存在d2l软件包中,供将来使用。
def data_iter(batch_size, features, labels):
num_examples = len(features)
indices = list(range(num_examples))
# The examples are read at random, in no particular order
random.shuffle(indices)
for i in range(0, num_examples, batch_size):
j = torch.tensor(indices[i: min(i + batch_size, num_examples)])
yield features[j], labels[j]
# 然后,"take "函数将根据索引返回相应的元素
为了建立一些直觉,让我们阅读和打印第一小批数据例子。每个小批数据中的特征形状告诉我们小批数据的大小和输入特征的数量。
同样地,我们的迷你批次的标签也会有一个由 batch_size 给出的形状。
batch_size = 10
for X, y in data_iter(batch_size, features, labels):
print(X, y)
break
tensor([[ 0.6135, -2.4877],
[-0.5844, -0.6766],
[-0.5871, -0.9422],
[ 0.7007, -0.1200],
[-1.2416, -0.1688],
[-0.6887, 0.3442],
[-0.2500, -0.7620],
[ 0.5054, 0.7916],
[-1.2516, -0.7632],
[-0.1521, 0.1231]]) tensor([13.8882, 5.3317, 6.2346, 6.0097, 2.2933, 1.6663, 6.2894, 2.5139,
4.2745, 3.4739])
毫不奇怪,当我们运行迭代器时,我们每次都会得到不同的小批,直到所有的数据都被用完(试试这个)。虽然上面实现的迭代器对于教学来说是很好的,但是它的效率很低,在实际问题上可能会给我们带来麻烦。例如,它要求我们在内存中加载所有的数据,并要求我们进行大量的随机内存访问。在Torch中实现的内置迭代器是相当有效的,它们可以处理存储在文件中的数据和通过数据流输入的数据。
三、初始化模型参数
在我们开始通过梯度下降优化我们的模型参数之前,我们首先需要有一些参数。在下面的代码中,我们通过从平均数为0、标准差为0.01的正态分布中抽取随机数来初始化权重,并将偏差𝑏设置为0。
w = torch.zeros(size=(num_inputs, 1)).normal_(std=0.01)
b = torch.zeros(size=(1,))
现在我们已经初始化了我们的参数,我们的下一个任务是更新它们,直到它们足够好地适合我们的数据。每次更新都需要获取我们的损失函数相对于参数的梯度(一个多维导数)。考虑到这个梯度,我们将在减少损失的方向上更新每个参数。
由于没有人愿意明确地计算梯度(这很繁琐而且容易出错),我们使用自动微分法来计算梯度。参见 :numref:chapter_autograd 获取更多细节。回顾autograd章节,为了让autograd知道它应该为我们的参数存储梯度,我们需要调用attach_grad函数,分配内存来存储我们计划采取的梯度。
w.requires_grad_(True)
b.requires_grad_(True)
tensor([0.], requires_grad=True)
四、定义模型
接下来,我们必须定义我们的模型,将其输入和参数与输出联系起来。回顾一下,为了计算线性模型的输出,我们只需将实例𝐗和模型权重𝑤的矩阵-向量点积,并在每个实例上加上偏移量𝑏。注意,下面的torch.matmul(X, w)是一个向量,b是一个标量。回顾一下,当我们把一个向量和一个标量相加时,标量会加到向量的每个分量上。
# This function has been saved in the d2l package for future use
def linreg(X, w, b):
return torch.matmul(X, w) + b
五、定义损失函数
由于更新我们的模型需要获取损失函数的梯度,我们应该首先定义损失函数。这里我们将使用上一节中描述的平方损失函数。在实施过程中,我们需要将真实值y转化为预测值的形状y_hat。以下函数返回的结果也将与y_hat的形状相同。
# 集成在dl2中了
def squared_loss(y_hat, y):
return (y_hat - y.reshape(y_hat.shape)) ** 2 / 2
六、定义优化算法
正如我们在上一节所讨论的,线性回归有一个封闭式的解决方案。然而,这不是一本关于线性回归的书,这是一本关于深度学习的书。由于本书介绍的其他模型都不能用分析法解决,我们将借此机会介绍你的第一个随机梯度下降(SGD)的工作实例。
在每一步,使用从我们的数据集中随机抽取的一个批次,我们将估计损失相对于我们的参数的梯度。然后,我们将在减少损失的方向上少量地更新我们的参数。假设梯度已经被计算出来,每个参数(param)的梯度已经被储存在param.grad中。下面的代码适用于SGD更新,给定一组参数、一个学习率和一个批次大小。更新步骤的大小由学习率lr决定。因为我们的损失是以一批例子的总和来计算的,所以我们用批次大小(batch_size)来规范我们的步骤大小,这样典型的步骤大小就不会严重依赖于我们对批次大小的选择。
# This function has been saved in the d2l package for future use
def sgd(params, lr, batch_size):
for param in params:
param.data.sub_(lr*param.grad/batch_size)
param.grad.data.zero_()
七、训练
现在我们已经有了所有的部件,我们准备实现主要的训练循环。理解这段代码至关重要,因为在你的深度学习生涯中,你会一次又一次地看到与这段代码几乎相同的训练循环。
在每个迭代中,我们将抓取迷你批次的模型,首先通过我们的模型来获得一组预测结果。在计算损失后,我们将调用反向函数通过网络进行反向传播,将与每个参数有关的梯度存储在其相应的.grad属性中。最后,我们将调用优化算法sgd来更新模型参数。由于我们之前将批次大小batch_size设置为10,所以每个小批次的损失形状l是(10,1)。
综上所述,我们将执行以下循环。
-
初始化参数 ( w , b ) (\mathbf{w}, b) (w,b)
-
重复执行,直到完成
-
计算梯度 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)B1∑i∈Bl(xi,yi,w,b)
-
更新参数 ( w , b ) ← ( w , b ) − η g (\mathbf{w}, b) \leftarrow (\mathbf{w}, b) - \eta \mathbf{g} (w,b)←(w,b)−ηg
在下面的代码中,l是minibatch中每个例子的损失的一个向量。因为l不是一个标量变量,运行l.backward()将l中的元素加在一起,得到新的变量,然后计算梯度。
在每个epoch(对数据的传递)中,我们将对整个数据集进行迭代(使用data_iter函数),对训练数据集中的每个例子进行一次传递(假设例子的数量可以被批处理的大小所除)。epochs的数量num_epochs和学习率lr都是超参数,我们在这里分别设置为3和0.03。不幸的是,设置超参数是很棘手的,需要通过试验和错误进行一些调整。我们暂时不考虑这些细节,但在后面的:numref:chapter_optimization中会对其进行修改。
lr = 0.03 # 学习率
num_epochs = 6 # 迭代次数
net = linreg # 线性模型
loss = squared_loss # 0.5 (y-y')^2
for epoch in range(num_epochs):
# 假设例子的数量可以被批量大小所划分,训练数据集中的所有例子都在一个历时迭代中使用一次。小批量例子的特征和标签分别由X和y给出
for X, y in data_iter(batch_size, features, labels):
l = loss(net(X, w, b), y) # X和y的最小批量损失
l.mean().backward() # 计算l相对于[w,b]的梯度
sgd([w, b], lr, batch_size) # 更新参数
with torch.no_grad():
train_l = loss(net(features, w, b), labels)
print('epoch %d, loss %f' % (epoch + 1, train_l.mean().numpy()))
epoch 1, loss 0.245055
epoch 2, loss 0.134116
epoch 3, loss 0.073418
epoch 4, loss 0.040199
epoch 5, loss 0.022022
epoch 6, loss 0.012077
在这种情况下,由于我们使用的是合成数据(我们自己合成的!),我们确切地知道真实参数是什么。因此,我们可以通过比较真实的参数和我们通过训练循环学到的参数来评估我们训练的成功。事实上,它们原来是非常接近的。
print('Error in estimating w', true_w - w.reshape(true_w.shape))
print('Error in estimating b', true_b - b)
Error in estimating w tensor([ 0.9384, -1.4121], grad_fn=<SubBackward0>)
Error in estimating b tensor([1.7719], grad_fn=<RsubBackward1>)
注意,我们不应该想当然地认为我们能够准确地恢复参数。这只发生在一个特殊类别的问题上:强凸优化问题,有 "足够的 "数据,以确保噪声样本允许我们恢复基本的依赖性。在大多数情况下,情况并非如此。事实上,深度网络的参数在两次不同的运行之间很少相同(甚至接近),除非所有的条件都相同,包括数据被遍历的顺序。然而,在机器学习中,我们通常不太关心恢复真正的基础参数,而更关心能导致准确预测的参数。幸运的是,即使在困难的优化问题上,该随机梯度下降往往可以导致非常好的解决方案,部分原因是对于我们将要处理的模型,存在许多工作良好的参数集。
八、总结
我们看到一个深度网络是如何实现的
并从头开始优化,只需使用torch.Tensor
和autograd
。
而不需要定义层,花哨的优化器等。
这只是触及了可能的表面。
在下面的章节中,我们将描述额外的模型
基于我们刚刚介绍的概念
并学习如何更简洁地实现它们。
九、练习
1、如果我们初始化权重𝐰=0,会发生什么?这个算法还能工作吗?
2、假设你是Georg Simon Ohm,想在电压和电流之间建立一个模型。你能不能用autograd来学习你的模型的参数。
3、你能用普朗克定律来确定一个物体的温度,使用光谱能量密度。
4、如果你想把autograd扩展到二阶导数,你可能遇到什么问题?你将如何解决这些问题?
5、为什么在squared_loss函数中需要reshape函数?
6、使用不同的学习率进行实验,找出损失函数值下降的速度。
7、如果例子的数量不能除以批处理的大小,data_iter函数的行为会发生什么?