写在前面的话:最近开始学习PyTorch,对其整个框架有些不熟悉,本文的目的就是对PyTorch的框架进行梳理。
本文是在文章“Understanding PyTorch with an example: a step-by-step tutorial”的基础上完成的,基本上是对文章的翻译,为了保证理解的正确性,专业术语均使用了原文。
建议大家有时间去阅读以下英文的原文。网站:https://towardsdatascience.com/understanding-pytorch-with-an-example-a-step-by-step-tutorial-81fc5f8c4e8e 查看
文章目录
1、 Introduction
PyTorch是发展最快的深度学习框架, PyTorch也是Python风格的,也就是说,如果您已经是Python开发人员,那么使用它会更自然。
2、Motivation
现在有许多PyTorch教程,其文档非常完整和广泛。 那么,为什么还要继续阅读本分步教程呢?
在本文中,将指导大家了解PyTorch如何使得通过Python能够更加简单、直观的构建一个深度学习模型,包含autograd,dynamic computation graph,model classes等,同时,本文还会展示如何避免在创建模型时常见的陷阱和错误。
3、A Simple Regression Problem
大多数教程都从一些漂亮的图像分类问题入手,以说明如何使用PyTorch。 这样可能看起来很酷,但我相信它会分散您的主要目标:PyTorch的工作原理?
因此,在本教程中,我将坚持一个简单且熟悉的问题:具有单个特征x的线性回归!
y = a + b x + ε y = a +bx+ \varepsilon y=a+bx+ε
Data Generation
数据生成中,我们使用一个维度为100的向量,作为特征 x x x,并设置 a = 1 , b = 2 a = 1, b = 2 a=1,b=2和一些高斯噪声 ε \varepsilon ε来创建标签。
之后,将原始的数据分为训练集和验证集,为了保证训练的有效性,首先对数据打乱,然后取前80个点作为训练集,后面20个点作为验证集,示例代码如下:
# Data Generation
np.random.seed(42)
x = np.random.rand(100, 1)
y = 1 + 2 * x + .1 * np.random.randn(100, 1)
# Shuffles the indices
idx = np.arange(100)
np.random.shuffle(idx)
# Uses first 80 random indices for train
train_idx = idx[:80]
# Uses the remaining indices for validation
val_idx = idx[80:]
# Generates train and validation sets
x_train, y_train = x[train_idx], y[train_idx]
x_val, y_val = x[val_idx], y[val_idx]
4、Gradient Descent
如果您对渐变下降的内部结构感到满意,请随时跳过此部分。 超出了本文的讨论范围,无法全面说明梯度下降的工作原理,但我将介绍计算梯度下降所需的四个基本步骤。
如果你对梯度下降的内容比较熟悉,可以随时跳过本部分。由于篇幅的限制,本文无法完全介绍梯度下降的工作原理,只是对计算梯度下降时所需的四个基本步骤进行说明。
Step1:Computer Loss
对于回归问题,损失是由Mean Square Error(MSE) 定义,也就是说,所有真实标签 y y y和预测值 ( a + b x ) (a+bx) (a+bx)之间差异的平方的均值
值得一提的是,如果我们使用训练集中(N)中的所有点来计算损失,那么我们将执行批量梯度下降(batch gradient descent)。 如果我们每次都使用一个点,那将是随机梯度下降(stochastic gradient descent)。 在1到N之间的其他任何(n)都表示一个小批量梯度下降(mini-batch gradient descent.)。
M S E = 1 N ∑ i = 1 N ( y i − y ^ i ) 2 M S E=\frac{1}{N} \sum_{i=1}^{N}\left(y_{i}-\widehat{y}_{i}\right)^{2} MSE=N1∑i=1N(yi−y i)2
M S E = 1 N ∑ i = 1 N ( y i − a − b x i ) 2 M S E=\frac{1}{N} \sum_{i=1}^{N}\left(y_{i}-a-b x_{i}\right)^{2} MSE=N1∑i=1N(yi−a−bxi)2
Step2: Compute the Gradients
梯度是偏导数——为什么会是偏导数呢? 因为有人会针对 with respect to(一般缩写为w.r.t) 单个参数进行计算。 我们有两个参数,a和b,因此我们必须计算两个偏导数。
当稍微改变一些其他数量时,导数告诉给定数量发生了多少变化。 在本文的例子中,偏导数反应了当我们改变两个参数中的每个参数时,我们的MSE损失会发生多少变化。
下面等式的最右边部分,是简单线性回归中的梯度下降计算。 整个公式将中间的链式求导法则列了出来,从而给出整个表达式是怎么计算出来的。
∂ M S E ∂ a = ∂ M S E ∂ y ^ i ⋅ ∂ y ^ i ∂ a = 1 N ∑ i = 1 N 2 ( y i − a − b x i ) ⋅ ( − 1 ) = − 2 1 N ∑ i = 1 N ( y i − y ^ i ) \frac{\partial M S E}{\partial a}=\frac{\partial M S E}{\partial \hat{y}_{i}} \cdot \frac{\partial \hat{y}_{i}}{\partial a}=\frac{1}{N} \sum_{i=1}^{N} 2\left(y_{i}-a-b x_{i}\right) \cdot(-1)=-2 \frac{1}{N} \sum_{i=1}^{N}\left(y_{i}-\hat{y}_{i}\right) ∂a∂MSE=∂y^i∂MSE⋅∂a∂y^i=N1∑i=1N2(yi−a−bxi)⋅(−1)=−2N1∑i=1N(yi−y^i)
∂ M S E ∂ b = ∂ M S E ∂ y ^ i ⋅ ∂ y ^ i ∂ b = 1 N ∑ i = 1 N 2 ( y i − a − b x i ) ⋅ ( − x i ) = − 2 1 N ∑ i = 1 N x i ( y i − y ^ i ) \frac{\partial M S E}{\partial b}=\frac{\partial M S E}{\partial \hat{y}_{i}} \cdot \frac{\partial \hat{y}_{i}}{\partial b}=\frac{1}{N} \sum_{i=1}^{N} 2\left(y_{i}-a-b x_{i}\right) \cdot\left(-x_{i}\right)=-2 \frac{1}{N} \sum_{i=1}^{N} x_{i}\left(y_{i}-\hat{y}_{i}\right) ∂b∂MSE=∂y^i∂MSE⋅∂b∂y^i=N1∑i=1N2(yi−a−bxi)⋅(−xi)=−2N1∑i=1Nxi(yi−y^i)
Step3:Update the Parameters
在最后一步,我们使用梯度来更新参数。 由于我们正在尝试将损失降到最低,因此在参数更新中,我们选择让梯度下降的方向进行更新。
还有一个参数需要考虑:学习率,用 η \eta η表示,他是我们需要应用于梯度以进行参数更新的乘法因子。
在本文的例子中,需要更新的参数有a和b,其更新公式如下:
a = a − η ∂ M S E ∂ a a=a-\eta \frac{\partial M S E}{\partial a} a=a−η∂a∂MSE
b = b − η ∂ M S E ∂ b b=b-\eta \frac{\partial M S E}{\partial b} b=b−η∂b∂MSE
在机器学习/深度学习中,学习率实则为一个超参数,如何选择适当的学习率也是一个单独的话题。
由于篇幅的限制,本文不再对学习率的选择进行赘述。
Step4: Rinse and Repeat
现在,我们使用更新后的参数返回到步骤1,然后重新启动该过程。
只要已经将每个点都用于计算损失,一个epoch就完成了。 对于 batch gradient descent而言,这是微不足道的,因为它使用所有点来计算损耗,也就是说一个epoc就等价于一次参数更新。 对于stochastic gradient descent,一个epoch意味着N次的参数更新,而对于minibatch gradient descent(大小为n),一个epoch对应N / n个更新。
概括地说,反复多次重复此过程就是训练模型。
5、Linear Regression in Numpy
是时候使用仅使用Numpy的梯度下降来实现我们的线性回归模型了。
为什么首先使用Numpy呢?本教程可是关于PyTorch的!原因见下文。
首先使用Numpy的两个目的:
- 首先,介绍我们的任务结构,该结构在很大程度上保持不变;
- 其次,展示主要的痛点,以便充分了解PyTorch更加方便的原因
为了训练模型,需要有以下两个初始化的步骤:
- 随机初始化参数/权重(本文的例子中,仅有两个,a和b)——对应下述代码的line3和line4
- 初始化超参数(在本文的例子中,有两个超参数,分别是学习率 η \eta η 和epoch的数量)——对应代码中的line9和line11
对于每个epoch,有如下四个训练步骤:
- 计算模型的预测值——也就是正向计算过程(Forward pass)——对应代码的line15
- 使用预测值、标签以及适合当前任务的损失函数,计算损失——对应代码的line18和line20
- 计算每一个参数的梯度——对应于代码中的line23和line24
- 更新所有参数——对应于代码中的line27和line28
注意,如果不使用 batch gradient descent (本文的示例中采用的是 batch gradient descent ),则必须有一个内部循环来为每个点 (stochastic gradient descent) 或n个点 (minibatch gradient descent) 来执行四个训练步骤 (即,每次训练为1个epoch,要把所有的训练数据过一遍 )。稍后会有一个minibatch的示例。
# Initializes parameters "a" and "b" randomly
np.random.seed(42)
a = np.random.randn(1)
b = np.random.randn(1)
print(a, b)
# Sets learning rate
lr = 1e-1
# Defines number of epochs
n_epochs = 1000
for epoch in range(n_epochs):
# Computes our model's predicted output
yhat = a + b * x_train
# How wrong is our model? That's the error!
error = (y_train - yhat)
# It is a regression, so it computes mean squared error (MSE)
loss = (error ** 2).mean()
# Computes gradients for both "a" and "b" parameters
a_grad = -2 * error.mean()
b_grad = -2 * (x_train * error).mean()
# Updates parameters using gradients and the learning rate
a = a - lr * a_grad
b = b - lr * b_grad
print(a, b)
# Sanity Check: do we get the same results as our gradient descent?
from sklearn.linear_model import LinearRegression
linr = LinearRegression()
linr.fit(x_train, y_train)
print(linr.intercept_, linr.coef_[0])
为了确保我们在代码中没有做任何错误,我们可以使用Scikit-Learn的线性回归来拟合模型并比较系数。可以看出,它们最多匹配6个小数位。
因此,使用Numpy,我们可以完全实现线性回归。
# a and b after initialization
[0.49671415] [-0.1382643]
# a and b after our gradient descent
[1.02354094] [1.96896411]
# intercept and coef from Scikit-Learn
[1.02354075] [1.96896447]
6、PyTorch
首先,我们需要介绍一些基本概念。
在深度学习中,我们到处都可以看到Tensor。 Google的框架之所以被称为TensorFlow是有原因的! 无论如何,Tensor是什么?
关于Tensor的说明,也可查看我的另外一篇博文:什么是Tensor?(张量) Dan Fleisch视频学习记录
Tensor
在Numpy中,从技术的角度严格将,三位的数组就是一个Tensor了。
在数学中,我们有这样的定义:标量scalar(也就是单个数字)的维度为0,向量vector的维度是1,矩阵matrix的维度是2,而张量Tensor的维度为3或者更更高。但是,为了简单起见, 我们通常将vector和matrix也称之为Tensor。
Loading Data,Devices and CUDA
问题1: 如何从Numpy的阵列变为PyTorch的张量?
实际中使用PyTorch中的from_numpy()
函数就可以了,但是需要注意的是,这个函数的返回值为一个CPU的Tensor。
问题2: 但是我想使用GPU,该怎么办?
不用担心,使用to()
函数即可。 它将Tensor发送到您指定的任何设备,包括GPU(称为cuda或cuda:0)。
问题3: 如果没有GPU,我希望代码回退到CPU怎么办?
这个也不需要担心。PyTorch中提供了cuda.is_available()
函数,该函数可以检测当前是否有可用的GPU,从而增加了您代码的鲁棒性。
此外,在计算的过程中,还可以使用float()
函数,将数据的精度转换为较低的精度(32位浮点数)
import torch
import torch.optim as optim
import torch.nn as nn
from torchviz import make_dot
device = 'cuda' if torch.cuda.is_available() else 'cpu'
# Our data was in Numpy arrays, but we need to transform them into PyTorch's Tensors
# and then we send them to the chosen device
x_train_tensor = torch.from_numpy(x_train).float().to(device)
y_train_tensor = torch.from_numpy(y_train).float().to(device)
# Here we can see the difference - notice that .type() is more useful
# since it also tells us WHERE the tensor is (device)
print(type(x_train), type(x_train_tensor), x_train_tensor.type())
如果获取这两个变量的数据类型,可以得到x_train
的数据类型是numpy.ndarray
,x_train_tensor
的数据类型是torch.Tensor
通过使用type()
函数,可以显示当前Tensor是在CPU还是在GPU中。
同样,如果我们想将一个Tensor变量转换为Numpy的变量,只需要使用numpy()
即可,但是需要注意的是,GPU的Tensor是不能够转换成Numpy的!!!
Creating Parameters
我们如何区分用于data的Tensor,和用来当做可训练的parameter/weight的Tensor呢?
对于parameter/weight,我们需要计算梯度,然后更新他们的值。这就是PyTorch中requires_grad=true
的作用,这个参数告诉了PyTorch我们需要它来计算梯度。
# FIRST
# Initializes parameters "a" and "b" randomly, ALMOST as we did in Numpy
# since we want to apply gradient descent on these parameters, we need
# to set REQUIRES_GRAD = TRUE
a = torch.randn(1, requires_grad=True, dtype=torch.float)
b = torch.randn(1, requires_grad=True, dtype=torch.float)
print(a, b)
# SECOND
# But what if we want to run it on a GPU? We could just send them to device, right?
a = torch.randn(1, requires_grad=True, dtype=torch.float).to(device)
b = torch.randn(1, requires_grad=True, dtype=torch.float).to(device)
print(a, b)
# Sorry, but NO! The to(device) "shadows" the gradient...
# THIRD
# We can either create regular tensors and send them to the device (as we did with our data)
a = torch.randn(1, dtype=torch.float).to(device)
b = torch.randn(1, dtype=torch.float).to(device)
# and THEN set them as requiring gradients...
a.requires_grad_()
b.requires_grad_()
print(a, b)
我们对上述代码进行解析。
在我们的示例中,只有a和b两个参数,因此第一段代码针对这两个parameters创建了两个Tensor,需要注意的是,他们是CPU的Tensor。
# FIRST
tensor([-0.5531], requires_grad=True)
tensor([-0.7314], requires_grad=True)
在第二段代码中,我们尝试将Tensor放置到GPU中的简单办法,从结果来看我们成功的把他们放到了GPU中,但是不知什么原因,“丢失了”梯度。
# SECOND
tensor([0.5158], device='cuda:0', grad_fn=<CopyBackwards>)
tensor([0.0246], device='cuda:0', grad_fn=<CopyBackwards>)
第三段代码中,我们先将张量发送到设备,然后使用requires_grad_()
方法将其requires_grad
设置为True
。
# THIRD
tensor([-0.8915], device='cuda:0', requires_grad=True)
tensor([0.3616], device='cuda:0', requires_grad=True)
在PyTorch中,每个以下划线(_)结尾的方法都会就地进行更改,这意味着它们将修改基础变量
尽管最后一种方法很好用,但在创建张量时将张量分配给设备要好得多。
# We can specify the device at the moment of creation - RECOMMENDED!
torch.manual_seed(42)
a = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)
b = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)
print(a, b)
运行结果如下:
tensor([0.6226], device='cuda:0', requires_grad=True)
tensor([1.4505], device='cuda:0', requires_grad=True)
7、Autograd
Autograd是PyTorch的自动微分软件包。 多亏了它,我们不必担心偏导数,链式规则或类似的东西,因此在PyTorch中我们仅需要使用backword()
函数,即可计算相应的导数,从而实现反向传播过程。
如前文所述,我们计算Loss相对parameter的偏导。因此,使用相应的变量调用backward()
函数即可,在本例中,可以使用loss.backword()
梯度的实际值是什么呢? 我们可以通过查看张量的grad
属性来检查它们。如果您查看该方法的文档,它会明确指出梯度是累积的。 因此,每次我们使用梯度来更新参数时,我们都需要随后将梯度归零。 这就是zero_()
的作用。
因此,让我们放弃对梯度的手动计算,而改用backword()
和zero_()
方法。
就这要吗? 嗯,几乎……但是,总有一个陷阱,这一次与参数的更新有关……
lr = 1e-1
n_epochs = 1000
torch.manual_seed(42)
a = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)
b = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)
for epoch in range(n_epochs):
yhat = a + b * x_train_tensor
error = y_train_tensor - yhat
loss = (error ** 2).mean()
# No more manual computation of gradients!
# a_grad = -2 * error.mean()
# b_grad = -2 * (x_tensor * error).mean()
# We just tell PyTorch to work its way BACKWARDS from the specified loss!
loss.backward()
# Let's check the computed gradients...
print(a.grad)
print(b.grad)
# What about UPDATING the parameters? Not so fast...
# FIRST ATTEMPT
# AttributeError: 'NoneType' object has no attribute 'zero_'
# a = a - lr * a.grad
# b = b - lr * b.grad
# print(a)
# SECOND ATTEMPT
# RuntimeError: a leaf Variable that requires grad has been used in an in-place operation.
# a -= lr * a.grad
# b -= lr * b.grad
# THIRD ATTEMPT
# We need to use NO_GRAD to keep the update out of the gradient computation
# Why is that? It boils down to the DYNAMIC GRAPH that PyTorch uses...
with torch.no_grad():
a -= lr * a.grad
b -= lr * b.grad
# PyTorch is "clingy" to its computed gradients, we need to tell it to let it go...
a.grad.zero_()
b.grad.zero_()
print(a, b)
在第一次尝试中,如果我们使用与Numpy代码中相同的更新结构,则会在下面得到奇怪的错误……但是通过查看张量本身,我们可以了解发生了什么——重新分配更新结果到我们的参数中的同时,我们再一次的“丢失”了梯度。 因此,grad属性原来是None,它会引发错误……
# FIRST ATTEMPT
tensor([0.7518], device='cuda:0', grad_fn=<SubBackward0>)
AttributeError: 'NoneType' object has no attribute 'zero_'
为什么?! 事实证明这是“too much of a good thing”的情况。 罪魁祸首是PyTorch能够从涉及任何梯度计算张量或其依赖项的每个Python操作中构建动态计算图的能力。
在下一部分中,我们将更深入地研究动态计算图的内部工作原理。
那么,我们如何告诉PyTorch“back off”并让我们更新参数而又不弄乱其动态计算图呢? 这就是torch.no_grad()
的优点。 它使我们能够对张量执行常规的Python操作,而与PyTorch的计算图无关。
最后,我们设法成功运行了模型并获得了结果参数。 确实,它们与我们在仅Numpy的实现中获得的匹配。
# THIRD ATTEMPT
tensor([1.0235], device='cuda:0', requires_grad=True) tensor([1.9690], device='cuda:0', requires_grad=True)
8、Dynamic Computation Graph
“Unfortunately, no one can be told what the dynamic computation graph is. You have to see it for yourself.” Morpheus
因此,让我们从最简单的开始:在我们的示例中,有两个需要对parameters、predictions、errors和loss计算梯度的Tensor
torch.manual_seed(42)
a = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)
b = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)
yhat = a + b * x_train_tensor
error = y_train_tensor - yhat
loss = (error ** 2).mean()
如果调用make_dot(yhat)
,将在下面的图中获得最左侧的图形:
下面我们仔细的对图中的各个组件进行分析。
- 蓝色框:这些与我们用作parameters的Tensor相对应,也就是我们要求PyTorch计算的Tensor;
- 灰色框:涉及梯度计算Tensor或其依赖项的Python操作;
- 绿色框:与灰色框相同,只是它是计算梯度的起点(假设从用于可视化图形的变量中调用了reverse()方法)—它们是从图形的底部向上计算的。
如果我们绘制误差(中)和损失(右)变量的图,则它们与第一个变量之间的唯一区别是中间步骤的数量(灰色框)。
现在,仔细看一下最左边图形的绿色框:有两个箭头指向它,因为它累加了两个变量 a a a和 b ∗ x b * x b∗x。 看起来很明显,对不对?
然后,查看同一图的灰色框:它正在执行乘法,即 b ∗ x b * x b∗x。 但是只有一个箭头指向它! 箭头来自与我们的参数b对应的蓝色框。
为什么我们没有数据x的框? 答案是:我们不计算它的梯度! 因此,即使计算图执行的操作中涉及更多Tensor,它也仅显示梯度计算Tensor及其相关量。
如果我们将参数a的require_grad设置为False,计算图将会发生什么?
毫不奇怪,与参数a对应的蓝色框不再存在! 很简单的道理:no gradients, no graph
关于动态计算图的最好之处在于,您可以根据需要使其复杂。 您甚至可以使用控制流语句(例如,if语句)来控制渐变的流
9、Optimizer
到目前为止,我们一直在使用计算出的梯度来手动更新参数。 这对于两个参数可能很好…但是如果我们有很多参数呢? 我们使用PyTorch的优化器之一,例如SGD或Adam。
优化器获取我们要更新的参数、要使用的学习率(可能还有许多其他超参数),并通过其step()
方法执行更新。
此外,我们也无需再将梯度逐个归零。 我们只需要调用优化器的zero_grad()
方法即可!
在下面的代码中,我们创建一个随机梯度下降(SGD)优化器来更新参数a和b。
不要被优化器的名称所迷惑:如果我们一次使用所有训练数据进行更新(就像我们在代码中实际所做的那样),那么优化器将执行批量梯度下降,尽管它的名称如此。
torch.manual_seed(42)
a = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)
b = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)
print(a, b)
lr = 1e-1
n_epochs = 1000
# Defines a SGD optimizer to update the parameters
optimizer = optim.SGD([a, b], lr=lr)
for epoch in range(n_epochs):
yhat = a + b * x_train_tensor
error = y_train_tensor - yhat
loss = (error ** 2).mean()
loss.backward()
# No more manual update!
# with torch.no_grad():
# a -= lr * a.grad
# b -= lr * b.grad
optimizer.step()
# No more telling PyTorch to let gradients go!
# a.grad.zero_()
# b.grad.zero_()
optimizer.zero_grad()
print(a, b)
让我们检查一下之前和之后的两个参数,以确保一切正常。
# BEFORE: a, b
tensor([0.6226], device='cuda:0', requires_grad=True) tensor([1.4505], device='cuda:0', requires_grad=True)
# AFTER: a, b
tensor([1.0235], device='cuda:0', requires_grad=True) tensor([1.9690], device='cuda:0', requires_grad=True)
10、Loss
现在,我们处理损失计算。 不出所料,PyTorch再次让我们获得了覆盖。 根据手头的任务,有很多损失函数可供选择。 由于我们是回归,因此我们使用均方误差(MSE)损失。
请注意,
nn.MSELoss
实际上为我们创建了一个损失函数,而不是损失函数本身。 此外,您可以指定要应用的归约方法,即,如何汇总单个点的结果-您可以将它们取平均值(reduction=’mean’
)或简单地求和(reduction=’sum‘
) 。
然后,我们稍后在第20行使用已创建的损失函数,根据给定的预测和标签来计算损失。
torch.manual_seed(42)
a = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)
b = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)
print(a, b)
lr = 1e-1
n_epochs = 1000
# Defines a MSE loss function
loss_fn = nn.MSELoss(reduction='mean')
optimizer = optim.SGD([a, b], lr=lr)
for epoch in range(n_epochs):
yhat = a + b * x_train_tensor
# No more manual loss!
# error = y_tensor - yhat
# loss = (error ** 2).mean()
loss = loss_fn(y_train_tensor, yhat)
loss.backward()
optimizer.step()
optimizer.zero_grad()
print(a, b)
此时,只有一小段代码需要更改:预测。 然后是时候介绍PyTorch的实施方法了。
11、Model
在PyTorch中,模型由继承自Module类的常规Python类表示。
它需要实现的最基本方法是:
__init__(self)
:它定义了组成模型的部分——在我们的例子中,是两个参数a和b。
不仅限于定义parameters…model还可以包含其他model(或layer)作为其属性,因此您可以轻松地嵌套它们。 不久我们还将看到一个示例。
forward(self, x)
:它执行实际的计算,即在给定输入x的情况下输出预测。
但是,你不应调用
forward(x)
方法。 应该调用整个model(x)
,因为model(x)
包含前向传递和输出预测。
让我们为回归任务建立一个适当的模型(但很简单)。 它看起来应该像这样:
class ManualLinearRegression(nn.Module):
def __init__(self):
super().__init__()
# To make "a" and "b" real parameters of the model, we need to wrap them with nn.Parameter
self.a = nn.Parameter(torch.randn(1, requires_grad=True, dtype=torch.float))
self.b = nn.Parameter(torch.randn(1, requires_grad=True, dtype=torch.float))
def forward(self, x):
# Computes the outputs / predictions
return self.a + self.b * x
在__init__
方法中,我们使用Parameter()
类定义了两个参数a和b,以告诉PyTorch这些张量应视为模型的属性
我们为什么要在乎呢? 这样,我们可以使用模型的parameters()
方法来检索所有模型参数(包括嵌套模型的那些参数)上的迭代器,以便将其用于优化程序(而不是自己构建参数列表!)。
此外,我们可以使用模型的state_dict()
方法获取所有参数的当前值。
重要:models和datas应该在同一个设备中,所以首先需要将models发送到datas所在的设备中;如果说我们的datas在GPU中(也就是使用GPU的Tensor),我们的models必须也要放到GPU中。
我们可以使用所有这些方便的方法来更改我们的代码,该代码应如下所示:
torch.manual_seed(42)
# Now we can create a model and send it at once to the device
model = ManualLinearRegression().to(device)
# We can also inspect its parameters using its state_dict
print(model.state_dict())
lr = 1e-1
n_epochs = 1000
loss_fn = nn.MSELoss(reduction='mean')
optimizer = optim.SGD(model.parameters(), lr=lr)
for epoch in range(n_epochs):
# What is this?!?
model.train()
# No more manual prediction!
# yhat = a + b * x_tensor
yhat = model(x_train_tensor)
loss = loss_fn(y_train_tensor, yhat)
loss.backward()
optimizer.step()
optimizer.zero_grad()
print(model.state_dict())
现在,打印的语句将如下所示—参数a和b的最终值仍然相同,因此一切正常:
OrderedDict([('a', tensor([0.3367], device='cuda:0')), ('b', tensor([0.1288], device='cuda:0'))])
OrderedDict([('a', tensor([1.0235], device='cuda:0')), ('b', tensor([1.9690], device='cuda:0'))])
什么是model.train()
???
在PyTorch中,模型具有
train()
方法,该方法有些令人失望,它不执行训练步骤。 其唯一目的是将模型设置为训练模式。 为什么这很重要? 例如,某些模型可能使用诸如Dropout之类的机制,这些机制在训练和评估阶段具有不同的行为。
Nested Models
在我们的模型中,我们手动创建了两个参数以执行线性回归。 让我们使用PyTorch的线性模型作为我们自己的属性,从而创建一个嵌套模型。
尽管这显然是一个人为的示例,但由于我们几乎包装了基础模型,而没有在模型中添加任何有用的东西(或根本没有!),它很好地说明了这一概念。
在__init__
方法中,我们创建了一个包含嵌套线性模型的属性。
在 forward()
方法中,我们调用嵌套模型本身来实现前向传播 (注意,我们不是调用 self.linear.forward(x)
!)。
class LayerLinearRegression(nn.Module):
def __init__(self):
super().__init__()
# Instead of our custom parameters, we use a Linear layer with single input and single output
self.linear = nn.Linear(1, 1)
def forward(self, x):
# Now it only takes a call to the layer to make predictions
return self.linear(x)
现在,如果我们调用此模型的parameters()
方法,PyTorch将以递归的方式计算其属性的参数。 您可以使用类似以下的命令自己尝试:[* LayerLinearRegression().parameters()]
以获取所有参数的列表。 您还可以添加新的linear()
属性,即使您在向前传递中根本不使用它们,它们也仍会列在parameters()
下。
Sequential Models
我们的模型很简单……您可能会想:“为什么还要为此建立一个类呢?!” 好吧,你说的有道理…
对于简单模型,其通常使用逐层连接的结构,其中上一层的输出作为下一层的输入,也就是sequential model
在我们的案例中,我们将使用单个参数构建一个序列模型,即用于训练线性回归的Linear
层。 该模型如下所示:
# Alternatively, you can use a Sequential model
model = nn.Sequential(nn.Linear(1, 1)).to(device)
Simple enough, right?
Training Step
到目前为止,我们已经定义了optimizer、loss function 和 model。如果我们使用不同的optimizer、loss function 或 model,它会改变吗?如果不会,我们如何使它更通用?
好吧,我想我们可以说,所有的代码都是在考虑到三个要素(optimizer, loss and model)、features和labels下,执行的训练步骤。
那么,如何编写一个函数,该函数使用了这三个要素,并且返回了另外一个用来执行训练步骤的函数, 将feature和label作为参数,并返回相应的loss呢?
然后,我们可以使用该通用函数来构建一个train_step()
函数,以在我们的训练循环中调用该函数。 现在我们的代码应如下所示……看看训练循环有多小?
def make_train_step(model, loss_fn, optimizer):
# Builds function that performs a step in the train loop
def train_step(x, y):
# Sets model to TRAIN mode
model.train()
# Makes predictions
yhat = model(x)
# Computes loss
loss = loss_fn(y, yhat)
# Computes gradients
loss.backward()
# Updates parameters and zeroes gradients
optimizer.step()
optimizer.zero_grad()
# Returns the loss
return loss.item()
# Returns the function that will be called inside the train loop
return train_step
# Creates the train_step function for our model, loss function and optimizer
train_step = make_train_step(model, loss_fn, optimizer)
losses = []
# For each epoch...
for epoch in range(n_epochs):
# Performs one train step and returns the corresponding loss
loss = train_step(x_train_tensor, y_train_tensor)
losses.append(loss)
# Checks model's parameters
print(model.state_dict())
让我们休息一下,然后专注于我们的数据……到目前为止,我们仅使用了Numpy阵列转换的PyTorch张量。 但是我们可以做得更好,我们可以建立一个数据集
12、Dataset
在PyTorch中,数据集由继承自Dataset类的常规Python类表示。 您可以将其视为一种Python元组列表,每个元组对应一个数据(features,label)。
它需要实现的最基本方法是:
__init__(self)
: 它采用构建元组列表所需的任何参数——它可能是将被加载和处理的CSV文件的名称; 它可能是两个张量,一个用于features,另一个用于label; 或其他任何东西,取决于手头的任务。
无需在构造函数方法(
__init__
)中加载整个数据集。 如果您的数据集很大(例如,成千上万个图像文件),则一次加载它不会节省内存。 建议按需加载它们(无论何时调用__get_item__
)。
__get_item__(self, index)
:它允许数据集被索引,因此它可以像列表一样工作(dataset[i]
)——它必须返回与请求的数据点相对应的元组(features,label)。 我们可以返回预加载的数据集或张量的相应切片,或者如上所述,按需加载它们(如本例所示)。__len__(self)
:它应该简单地返回整个数据集的大小,因此,无论何时采样,其索引都限于实际大小。
让我们构建一个简单的自定义数据集,该数据集使用两个张量作为参数:一个用于要素,一个用于标签。 对于任何给定的索引,我们的数据集类将返回每个张量的对应切片。 它看起来应该像这样:
from torch.utils.data import Dataset, TensorDataset
class CustomDataset(Dataset):
def __init__(self, x_tensor, y_tensor):
self.x = x_tensor
self.y = y_tensor
def __getitem__(self, index):
return (self.x[index], self.y[index])
def __len__(self):
return len(self.x)
# Wait, is this a CPU tensor now? Why? Where is .to(device)?
x_train_tensor = torch.from_numpy(x_train).float()
y_train_tensor = torch.from_numpy(y_train).float()
train_data = CustomDataset(x_train_tensor, y_train_tensor)
print(train_data[0])
train_data = TensorDataset(x_train_tensor, y_train_tensor)
print(train_data[0])
您可能会想“为什么要经历所有这些麻烦才能在一个类中包装几个张量?”。
如果一个数据集只不过是几个张量,我们可以使用PyTorch的TensorDataset类,该类将完成我们在上面的自定义数据集中所做的工作。
您是否注意到我们使用Numpy数组构建了训练张量,但没有将它们
to.device
? 因此,它们现在是CPU张量! 为什么?
我们不希望像现在在我们的示例中那样一直将整个训练数据加载到GPU张量中,因为它占用了我们宝贵的图形卡RAM中的空间。
13、DataLoad
到目前为止,我们在每个训练步骤中都使用了整个培训数据。 一直都是batch gradient descent。 当然,这对于我们的可笑的小数据集来说是很好的,但是如果我们要认真对待所有这些,就必须使用minibatch gradient descent。 因此,我们需要迷你批次。 因此,我们需要相应地对数据集进行切片。
在PyTorch中,DataLoader
类就是用于此工作的。我们只需要告诉他要使用的数据集(也就是我们在12节中刚刚构建的数据集),所需的mini-batch的batch size以及是否需要对数据集打乱即可。
我们的Loader的工作原理和迭代器类似,因此我们可以遍历数据集并每次获得不同的一个mini-batch
from torch.utils.data import DataLoader
train_loader = DataLoader(dataset=train_data, batch_size=16, shuffle=True)
要检索示例小批量,可以简单地运行以下命令——它会返回一个包含两个张量的列表,一个张量用于feature,另一个张量用于labels。
next(iter(train_loader))
这如何改变我们的训练循环? 让我们来看看!
losses = []
train_step = make_train_step(model, loss_fn, optimizer)
for epoch in range(n_epochs):
for x_batch, y_batch in train_loader:
# the dataset "lives" in the CPU, so do our mini-batches
# therefore, we need to send those mini-batches to the
# device where the model "lives"
x_batch = x_batch.to(device)
y_batch = y_batch.to(device)
loss = train_step(x_batch, y_batch)
losses.append(loss)
print(model.state_dict())
和之前相比,有两点不同:1、我们不仅有一个内部的循环从DataLoader
中依次加载每个mini-batch,2、更重要的是,我们现在每次只会将一个mini-batch送入到device(即GPU中,从而减少了GPU的内存资源消耗)
对于更大的数据集,使用
Dataset
的_get_item__
将样本一个个地加载CPU的Tensor上,然后依次将属于一个mini-batch的样本发送到GPU中,这样就可以充分利用你的显卡RAM。
此外,如果您有很多GPU来训练模型,则最好保持数据集“不可知”,并在训练过程中将批次分配给不同的GPU。
到目前为止,我们仅关注培训数据。 我们为其构建了一个Datasheet
和一个DataLoader
。 对于验证集,我们可以做同样的操作,和上文开始所述的分割,或者使用random_split
Random_split
PyTorch的random_split()
方法是执行训练验证拆分的一种简单而又熟悉的方法。 请记住,在我们的示例中,我们需要将其应用于整个数据集,然后,对于每个数据子集,我们构建一个对应的DataLoader,因此我们的代码如下所示:
from torch.utils.data.dataset import random_split
x_tensor = torch.from_numpy(x).float()
y_tensor = torch.from_numpy(y).float()
dataset = TensorDataset(x_tensor, y_tensor)
train_dataset, val_dataset = random_split(dataset, [80, 20])
train_loader = DataLoader(dataset=train_dataset, batch_size=16)
val_loader = DataLoader(dataset=val_dataset, batch_size=20)
现在,我们为验证集提供了一个数据加载器,因此,将其用于…
14、Evaluation
这是我们旅程的最后一部分——我们需要更改训练循环以包括对模型的评估,即计算验证损失。 第一步是包括另一个内部循环,以处理来自验证加载程序的mini-batch,并将其发送到与我们的模型相同的device。 接下来,我们使用模型进行预测(第23行),并计算相应的损失(第24行)。
差不多就可以了,但有两件事需要考虑,但很重要:
torch.no_grad()
: 尽管这不会对我们的简单模型有所影响,但最好还是使用此上下文管理器包装验证内部循环,以禁用可能无意触发的任何梯度计算——梯度属于训练过程,而不是验证步骤;eval()
:它唯一要做的就是将模型设置为评估模式(就像它的train()
一样),因此模型可以针对某些操作(例如Dropout
)调整其行为。
现在,我们的 training loop 是这样的:
losses = []
val_losses = []
train_step = make_train_step(model, loss_fn, optimizer)
for epoch in range(n_epochs):
for x_batch, y_batch in train_loader:
x_batch = x_batch.to(device)
y_batch = y_batch.to(device)
loss = train_step(x_batch, y_batch)
losses.append(loss)
with torch.no_grad():
for x_val, y_val in val_loader:
x_val = x_val.to(device)
y_val = y_val.to(device)
model.eval()
yhat = model(x_val)
val_loss = loss_fn(y_val, yhat)
val_losses.append(val_loss.item())
print(model.state_dict())
我们还有什么可以改善或改变的? 当然,总会有其他东西要添加到您的模型中——例如,使用学习率调度程序。 但是这篇文章已经太久了,所以我将在这里停止。
最后的代码
import numpy as np
import torch
import torch.optim as optim
import torch.nn as nn
from torchviz import make_dot
from torch.utils.data import Dataset, TensorDataset, DataLoader
from torch.utils.data.dataset import random_split
device = 'cuda' if torch.cuda.is_available() else 'cpu'
np.random.seed(42)
x = np.random.rand(100, 1)
true_a, true_b = 1, 2
y = true_a + true_b*x + 0.1*np.random.randn(100, 1)
x_tensor = torch.from_numpy(x).float()
y_tensor = torch.from_numpy(y).float()
class CustomDataset(Dataset):
def __init__(self, x_tensor, y_tensor):
self.x = x_tensor
self.y = y_tensor
def __getitem__(self, index):
return (self.x[index], self.y[index])
def __len__(self):
return len(self.x)
dataset = TensorDataset(x_tensor, y_tensor) # dataset = CustomDataset(x_tensor, y_tensor)
train_dataset, val_dataset = random_split(dataset, [80, 20])
train_loader = DataLoader(dataset=train_dataset, batch_size=16)
val_loader = DataLoader(dataset=val_dataset, batch_size=20)
class ManualLinearRegression(nn.Module):
def __init__(self):
super().__init__()
self.linear = nn.Linear(1, 1)
def forward(self, x):
return self.linear(x)
def make_train_step(model, loss_fn, optimizer):
def train_step(x, y):
model.train()
yhat = model(x)
loss = loss_fn(y, yhat)
loss.backward()
optimizer.step()
optimizer.zero_grad()
return loss.item()
return train_step
# Estimate a and b
torch.manual_seed(42)
model = ManualLinearRegression().to(device) # model = nn.Sequential(nn.Linear(1, 1)).to(device)
loss_fn = nn.MSELoss(reduction='mean')
optimizer = optim.SGD(model.parameters(), lr=1e-1)
train_step = make_train_step(model, loss_fn, optimizer)
n_epochs = 100
training_losses = []
validation_losses = []
print(model.state_dict())
for epoch in range(n_epochs):
batch_losses = []
for x_batch, y_batch in train_loader:
x_batch = x_batch.to(device)
y_batch = y_batch.to(device)
loss = train_step(x_batch, y_batch)
batch_losses.append(loss)
training_loss = np.mean(batch_losses)
training_losses.append(training_loss)
with torch.no_grad():
val_losses = []
for x_val, y_val in val_loader:
x_val = x_val.to(device)
y_val = y_val.to(device)
model.eval()
yhat = model(x_val)
val_loss = loss_fn(y_val, yhat).item()
val_losses.append(val_loss)
validation_loss = np.mean(val_losses)
validation_losses.append(validation_loss)
print(f"[{epoch+1}] Training loss: {training_loss:.3f}\t Validation loss: {validation_loss:.3f}")
print(model.state_dict())