通过示例了解PyTorch:分步教程

写在前面的话:最近开始学习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=N1i=1N(yiy 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=N1i=1N(yiabxi)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) aMSE=y^iMSEay^i=N1i=1N2(yiabxi)(1)=2N1i=1N(yiy^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) bMSE=y^iMSEby^i=N1i=1N2(yiabxi)(xi)=2N1i=1Nxi(yiy^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ηaMSE

b = b − η ∂ M S E ∂ b b=b-\eta \frac{\partial M S E}{\partial b} b=bηbMSE

在机器学习/深度学习中,学习率实则为一个超参数,如何选择适当的学习率也是一个单独的话题。

由于篇幅的限制,本文不再对学习率的选择进行赘述。

Step4: Rinse and Repeat

现在,我们使用更新后的参数返回到步骤1,然后重新启动该过程。

只要已经将每个点都用于计算损失,一个epoch就完成了。 对于 batch gradient descent而言,这是微不足道的,因为它使用所有点来计算损耗,也就是说一个epoc就等价于一次参数更新。 对于stochastic gradient descent,一个epoch意味着N次的参数更新,而对于mini­batch 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个点 (mini­batch gradient descent) 来执行四个训练步骤 (即,每次训练为1个epoch,要把所有的训练数据过一遍 )。稍后会有一个mini­batch的示例。

# 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.ndarrayx_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 bx。 看起来很明显,对不对?

然后,查看同一图的灰色框:它正在执行乘法,即 b ∗ x b * x bx。 但是只有一个箭头指向它! 箭头来自与我们的参数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())
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值