手把手教程,用例子让你理解PyTorch的精髓,非常值得一读!

点击上方“AI公园”,关注公众号,选择加“星标“或“置顶”


作者:Daniel Godoy

编译:ronghuaiyang

导读

网上有非常多的PyTorch的教程,但是这个教程很值得一读,结构化,增量化的学习PyTorch。


介绍

PyTorch增长最快的深度学习框架,Fast.ai也在其MOOC课程中,如Deep Learning for Coders 中也用到了它。

PyTorch也非常pythonic,这意味着,如果你已经是Python开发人员,那么使用它会感觉更自然。

此外,根据Andrej Karpathy的说法,使用PyTorch甚至可能改善您的健康:-)

动机

网上有非常的PyTorch教程,它的文档非常完整。那么,为什么你还要读我这个教程呢?

尽管人们可以找到关于PyTorch可以做的几乎所有事情的信息,但是没有一个从基本原则到它的结构逐渐来学习的方法。

在这篇文章中,我会让你了解PyTorch学起来很容易的主要原因和在Python中构建一个深度学习模型的更多直觉自动微分动态计算图 , 模型类等等,我也会告诉你如何避免一些常见的陷阱和错误

PyTorch

首先,我们需要介绍一些基本概念,如果在进行全面建模之前没有很好地掌握它们,这些概念可能会使你学起来感到吃力。

在深度学习中,我们随处可见tensors。嗯,谷歌的框架被称为TensorFlow是有原因的!那到底什么是张量?

张量

在Numpy中,你可能有一个三维数组,对吧?这是一种技术上的说法。

标量(单个数字)维度为零,向量维度为1,矩阵维度为2,张量的维度为3或者更多,就是这样了!

但是,为了让事情变得简单,我们通常把向量和矩阵也叫作张量,所以,从现在开始,所有的东西要么是标量,要么是张量!

手把手教程,用例子让你理解PyTorch的精髓,非常值得一读!_第1张图片

图1: 张量就是高纬度的矩阵T

加载数据,设备和CUDA

你可能会问:“如何从Numpy的数组转换到PyTorch的张量 ?”这就是 from_numpy的作用。不过,它返回一个CPU张量

“但是我想使用我的高级GPU……”,你说。不用担心,这就是 to()的好处。它将你的张量发送到你指定的任何设备,包括你的GPU(称为 cuda 或者 cuda:0)。

“如果没有GPU可用的话,我怎么让我的代码回退到CPU?“,你可以使用 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())
                                  加载数据: Numpy数组转化为PyTorch张量

如果你比较这两个变量的类型,你会得到:第一个是 numpy.ndarray,第二个是 torch.Tensor

但是你的张量放在哪里呢?在CPU还是GPU中?你不知道…你可以使用PyTorch的 type(),它将显示它的位置: torch.cuda.FloatTensor—这是一个GPU中的张量。

我们也可以反过来,使用 numpy()将张量转换回Numpy数组。它应该像 x_train_tensor.numpy()一样简单,但是…

TypeError: can't convert CUDA tensor to numpy. Use Tensor.cpu() to copy the tensor to host memory first.

不幸的是,Numpy不能处理GPU张量……你需要首先使用 cpu()使它们成为CPU张量。

创建变量

如何区分不同的张量 —就像我们刚刚创建的张量—用作(可训练)参数/权重 的张量

后一个张量需要计算它的梯度,所以我们可以更新它们的值(即参数的值)。这就是 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)
为参数创建变量

第一个代码块为我们的参数创建了两个很好的张量,具有梯度等等。但是它们是CPU张量。

# FIRST	
tensor([-0.5531], requires_grad=True)	
tensor([-0.7314], requires_grad=True)

在第二段代码中,我们尝试了将它们发送到GPU的naive方法。我们成功地将它们发送到另一个设备上,但不知怎的,我们“丢失”了梯度

# SECOND	
tensor([0.5158], device='cuda:0', grad_fn=) tensor([0.0246], device='cuda:0', grad_fn=)

在第三段代码中,我们首先把我们的张量发送到设置中,然后使用 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中,每一种以下划线结束的函数都说是in-place的,意思是改变的变量本身。

尽管最后一种方法工作得很好,但是最好是在创建张量的时候就把张量分配给设备

# 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)

现在我们知道了如何创建需要梯度的张量,让我们看看PyTorch如何处理它们。

自动微分

Autograd是PyTorch的自动微分包。多亏了它,我们不需要担心偏导,链式法则或类似的东西。

那么,我们如何告诉PyTorch执行它的操作并计算所有的梯度?这就是 backward()的好处。

你还记得计算梯度的起始点吗?就是损失,我们需要对参数计算它的偏导数。因此,我们需要从对应的Python变量调用 backward()方法,比如 loss.backward().

那么gradient *的实际值*是多少呢?我们可以通过查看张量的 grad 属性来看到。

如果看一下文档,可以清楚地看到梯度是累积的。所以,每次我们使用gradients更新参数时,我们需要把前面的梯度归零。这就是 zero_()的好处。

因此,让我们抛弃手动计算梯度,同时使用 backward()和 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=)	
AttributeError: 'NoneType' object has no attribute 'zero_'

然后,在第二次尝试中,我们使用熟悉的in-place Python赋值对其进行了轻微的更改。而且,PyTorch再次抱怨它并提出了一个错误

# SECOND ATTEMPT	
RuntimeError: a leaf Variable that requires grad has been used in an in-place operation.

为什么?!罪魁祸首是PyTorch能够从每一个Python操作中构建一个动态计算图,该操作涉及到任何梯度计算张量及其依赖项

在下一节中,我们将更深入地研究动态计算图的内部工作原理。

那么,我们如何告诉PyTorch “back off”,并让我们更新我们的参数,但不会打乱它的花哨的动态计算图呢?这就是 torch.no_grad()的好处。它允许我们对张量执行常规的Python操作,独立于PyTorch的计算图

最后,我们成功地运行了我们的模型并获得了结果参数。当然,它们和我们在Numpy only实现中得到的那些是匹配的。

# THIRD ATTEMPT	
tensor([1.0235], device='cuda:0', requires_grad=True) tensor([1.9690], device='cuda:0', requires_grad=True)

动态计算图

“不幸的是,没有人知道动态计算图是什么。你得亲眼看看。” Morpheus

PyTorchViz包及其 make_dot(variable)方法允许我们轻松地可视化与给定Python变量关联的图。

因此,让我们使用:两个(梯度计算)张量作为参数、预测、误差和损失。

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()
三步计算mse

如果我调用 make_dot(yhat)我们会得到下面最左边的图:

手把手教程,用例子让你理解PyTorch的精髓,非常值得一读!_第2张图片

图 2: 计算MSE的计算图的每一步

让我们仔细看看它的几个部分:

  • 蓝色块:这些是我们用作参数的张量,我们想让PyTorch来计算它们的梯度。

  • 灰色块:包括了梯度计算张量及其依赖的Python操作。

  • 绿色块:和灰色块一样,只不过它是gradients计算的起点。(假设 backward()方法是从用来进行可视化图形的变量调用的)—它们在图中是自底向上计算的。

如果我们为 error(中间)和 loss(右边)变量绘图,那么它们与第一个变量之间的惟一区别是中间步骤的数量(灰色框)。

现在,仔细看看最左边图的绿色框:有两个箭头指向它,因为它是将两个变量相加,即 a 和 b*x 。很明显吧?

然后,查看相同图的灰色框:它执行乘法 ,即 b*x。但是只有一个箭头指向它!箭头来自绿色框,它对应于我们的参数 b 。

为什么我们没有一个框来存放我们的数据x?答案是:我们不为它计算梯度 !因此,即使计算图执行的操作涉及多个张量,它只显示梯度计算张量及其依赖项

如果我们将参数a设置为 requires_grad为 False,计算图会发生什么?

手把手教程,用例子让你理解PyTorch的精髓,非常值得一读!_第3张图片

图 3: 现在变量不再计算梯度了,但是仍然参与计算

毫无疑问,与参数a对应的蓝色框不再存在!很简单:没有梯度,没有图形

动态计算图最好的一点是,你可以让它像你想要的那样复杂。您甚至可以使用控制流语句(例如if语句)来控制梯度流:-)

下面的图显示了一个例子。

手把手教程,用例子让你理解PyTorch的精髓,非常值得一读!_第4张图片

图 4: 复杂的计算图 :-)

优化器

到目前为止,我们一直在使用计算的梯度手动更新参数。这对于两个参数来说可能没问题,但是如果我们有多个参数呢?我们使用PyTorch的优化器,比如[SGDAdam

优化器获取我们想要更新的参数、我们想要使用的学习率(可能还有许多其他超参数!),并且通过它的 step()方法执行更新。

此外,我们也不需要一个一个地把梯度置为零。我们只需要调用优化器的 zero_grad()方法,就这样!

在下面的代码中,我们创建了一个随机梯度下降 (SGD)优化器来更新我们的参数ab

不要被优化器的名称所迷惑:如果我们同时使用所有的训练数据进行更新——正如我们在代码中所做的那样——优化器执行的是batch gradient descent,不要管它的名称是什么。

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)
PyTorch的优化器实际工作方法,不需要手动更新参数

让我们检查一下之前和之后的两个参数,以确保一切正常:

# 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)

太酷了!我们优化了优化流程:-)还剩下什么?

损失

我们现在处理损失的计算。正如我们所料,PyTorch又一次帮我们搞定了。根据手头的任务,可以选择许多loss function。由于我们的任务是一个回归,所以我们使用的是均方误差(MSE)损失。

注意, nn.MSELoss实际上为我们创建了一个损失函数 —它不是损失函数本身。此外,你还可以指定要应用的reduction method,即如何合并每个点的结果—你可以对它们进行平均(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损失函数的loss计算,不需要手工计算loss

此时,只剩下一段代码需要更改:*predictions *。现在是时候介绍PyTorch的方法来实现…

模型

在PyTorch中,model由一个常规的Python类表示,该类继承自Module类。

它需要实现的最基本的方法是:

  • __init__(self)定义了模型的构建部分—在我们的例子中,包括两个参数a 和b.

模型可以包含其他模型(或层)作为它的属性,所以可以很容易地嵌套它们。我们很快也会看到一个例子。

  • forward(self,x): 进行了实际的计算,也就是说,对于输入x,输出一个预测。

但是,你不应该调用 forward(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
构建“手工”模型,通过Parameter创建参数

在 __init__方法中,我们定义了两个参数ab,使用 Parameter()类告诉PyTorch这些张量应该被视为模型的参数

我们为什么要关心这个?通过这样做,我们可以使用模型的 Parameter()方法迭代检索模型的所有参数,甚至是嵌套模型的那些参数,我们可以使用它们来提供给优化器(而不是自己构建参数列表!)

此外,我们可以使用模型的 state_dict()方法获得所有参数的当前值

重要:我们需要将模型发送到数据所在的设备。如果我们的数据是由GPU张量构成的,我们的模型也必须“活”在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())
PyTorch的模型构建

现在,打印出来的语句将是这样的——参数ab的最终值仍然相同,所以一切正常:-)

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'))])

我希望你注意到了代码中的一个特殊语句,我给它写了一个注释,“What is this?!?” — model.train()

在PyTorch中,模型有一个 train()方法,但是,这个并不是执行训练步骤。其唯一目的是将模型设置为训练模式。为什么这很重要?例如,有些模型可能使用Dropout这样的机制,这些机制在训练和评估阶段具有不同的行为

嵌套模型

在我们的模型中,我们手动创建了两个参数来执行线性回归。让我们使用PyTorch的Linear模型作为我们自己的属性,从而创建一个嵌套模型。

尽管这显然是一个人为设计的例子,因为我们几乎是在包装底层模型而没有添加任何有用的东西(或者,根本没有!),但它很好地说明了这个概念。

在 __init__方法中,我们创建了一个属性,其中包含我们的嵌套 Linear 模型 。

在 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)
使用PyTorch的Linear layer构建模型

现在,如果我们调用这个模型的 parameters()方法,PyTorch将以递归方式显示其属性的参数。你可以使用类似于 LayerLinearRegression().parameters()这样的语句来获得所有参数的列表。你还可以添加新的 Linear属性,即使在前向传递中根本不使用它们,它们仍然会在 parameters()下列出。

顺序建模

我们的模型非常简单……你可能会想:“为什么还要为它创建一个类呢?”嗯,你说的有道理……

对于使用 run-of-the-mill layersstraightforward models,其中一层的输出按顺序作为下一层的输入,我们可以使用Sequential模型:-)

在我们的例子中,我们将用一个参数构建一个序列模型,即我们用来训练线性回归的 Linear层。模型应该是这样的:

# Alternatively, you can use a Sequential model	
model = nn.Sequential(nn.Linear(1, 1)).to(device)

非常简单,是不是?

训练步骤

到目前为止,我们已经定义了一个优化器、一个loss函数和一个model。向上滚动一点,快速查看循环内的代码。如果我们使用不同的优化器,或者loss,甚至model,它会改变吗?如果没有,我们如何使它更通用

好吧,我想我们可以说所有这些代码行执行一个训练步骤,给定这些三个元素(优化器、损失和模型)、特征标签

那么,如果编写一个函数,该函数接受这三个元素,然后返回执行训练步骤的另一个函数,将一组特征和标签作为参数并返回相应的损失,情况会如何呢?

然后,我们可以使用这个通用函数来构建一个 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())
构建一个函数来进行一个步骤的训练

让我们休息一下,暂时关注一下我们的data,到目前为止,我们只是使用了Numpy数组 转成PyTorch张量。但我们可以做得更好,我们可以建立一个……

Dataset

在PyTorch中,dataset由一个常规的Python类表示,该类继承自dataset类。你可以将它看作一种Python 元组列表,每个元组对应于一个数据点(特性、标签)

它需要实现的最基本的方法是:

  • __init__(self) : 它接受任何需要的参数建立一个元组列表—可能是一个CSV文件的名称,用来加载和处理,可能是两个张量,一个代表特征,另一个代表标签,或者其他什么,取决于手头的任务。

不需要在构造函数方法 ( __init__)中加载整个数据集。如果你的dataset很大(例如,成千上万的图像文件),立即加载不会提高内存效率。建议按需加载(每次调用 __get_item__的时候)。

  • __get_item__(self,index): 运行dataset进行索引,所以可以让dataset像列表一样进行操作 ( dataset[i]) —返回一个元组(特征,标签),对应所取到的数据点。我们可以返回我们预加载数据集的对应切片或则按需进行加载。

  • __len__(self): 返回整个数据集的大小,不管有没有被采样到,得到的是实际索引的上限的大小。

我们来构建一个简单的自定义数据集,它接受两个张量作为参数:一个用于特征,一个用于标签。对于任何给定的索引,我们的dataset类将返回每个张量的对应切片。它应该是这样的:

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数组构建了我们的训练张量,但是我们没有将它们发送到设备?所以,它们现在是CPU张量!为什么呢?

我们不希望我们的全部训练数据被加载到GPU中,就像我们到目前为止的例子中所做的那样,因为这样占用了我们宝贵的显卡RAM中的空间

好吧,不过话说回来,我们为什么要构建数据集呢?我们这么做是因为我们想用…

DataLoader

到目前为止,我们在每个训练步骤都使用了完整的训练数据。一直以来都是批梯度下降。当然,对于我们的小得可笑的数据集来说,这是可以的,但是如果我们想认真对待这一切,我们必须使用小批量梯度下降。因此,我们需要小批量。因此,我们需要相应地对数据集进行切片。你想“手工”做吗?我不想!

因此,我们使用PyTorch的DataLoader类来完成这项任务。我们告诉它使用哪个dataset(我们在前一节中刚刚构建的数据集)、所需的minibatch size,以及是否需要shuffle它。就是这样!

我们的加载器将像迭代器一样工作,所以我们可以循环它并*每次获取不同的minibatch *。

from torch.utils.data import DataLoader	
train_loader = DataLoader(dataset=train_data, batch_size=16, shuffle=True)
为我们的训练数据构建data loader

要检索一个mini-batch的样本,可以简单地运行下面的命令—它将返回一个包含两个张量的列表,一个用于特征,另一个用于标签。

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())
使用mini-batch梯度下降

现在有两件事不同了:不仅我们有一个内部循环来从我们的 DataLoader加载每个mini-batch,更重要的是,我们现在只向设备发送一个mini-batch。

对于更大的数据集,可以使用Dataset的 __get_item__ ,通过采样加载数据样本(到CPU的tensor中),然后把一个minibatch中所有的样本发送到GPU中,这样来最大程度利用我们的显卡显存。

此外,如果你有*多个gpu *来训练模型,那么最好保持你的数据集“agnostic”,并在训练期间将batch分配给不同的gpu。

到目前为止,我们只关注训练数据。我们为它构建了一个dataset和一个data loader。我们可以对validation数据执行同样的操作,使用我们在本文开头执行的split ,或者我们可以使用random_split。

随机划分

PyTorch的 random_split()方法是执行训练验证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)
把数据集划分为训练集合验证集,PyTorch的方法!

现在,我们的验证集有了一个数据加载器,因此,将它用于…

评估

这是我们的最后部分—我们需要更改训练循环,以包含对模型的评估,即计算验证损失。第一步是包含另一个内部循环来处理来自validation loader的mini-batch,将它们发送到与我们的模型相同的设备。接下来,我们使用模型(第23行)进行预测,并计算相应的损失(第24行)。

这就差不多了,但是还有两件事需要考虑:

  • torch.no_grad(): 虽然在我们的简单模型中不会有什么不同,但是用这个上下文管理器包装验证内部循环是一个好的实践,以禁用你可能无意中触发的任何梯度计算——梯度属于训练,验证步骤不需要。

  • eval(): 它所做的唯一一件事就是将模型设置为评估模式(就像它的 train()对应项所做的那样),这样模型就可以调整它的行为,比如Dropout

现在,我们的训练循环应该是这样的:

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())


计算验证损失


还有什么我们可以改进或改变的吗?当然,总是有其他东西可以添加到你的模型中——例如,使用学习率策略。但是这篇文章已经太长了,所以我就到此为止。

“带有所有花哨功能的完整工作代码在哪里?”你可以在这里找到它:https://gist.github.com/dvgodoy/1d818d86a6a0dc6e7c07610835b46fe4。

最后的想法

虽然这篇文章是比我预期的长了,但是我不会让它有所不同——我相信这是人们进行学习的时候最必要的步骤,结构化,增量化的方法来学习如何使用PyTorch开发深度学习模型。

希望在完成本文中的所有代码之后,你能够更好地理解PyTorch的官方教程,并更轻松地学习它。

640?wx_fmt=png— END—

英文原文:https://towardsdatascience.com/understanding-pytorch-with-an-example-a-step-by-step-tutorial-81fc5f8c4e8e

640?wx_fmt=jpeg

请长按或扫描二维码关注本公众号

喜欢的话,请给我个好看吧640?wx_fmt=gif

你可能感兴趣的:(手把手教程,用例子让你理解PyTorch的精髓,非常值得一读!)