通过案例学PyTorch。
微信搜索关注《Python学研大本营》,加入读者群,分享更多精彩
模型
在 PyTorch 中,模型由继承自Module类的常规Python 类表示。
它需要实现的最基本的方法是:
-
__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
构建我们的“手动”模型,逐个创建参数
在该__init__
方法中,我们使用类定义了两个参数a和b,使用Parameter()
类,告诉 PyTorch 这些张量应被视为模型的参数,它们是 的属性。
我们为什么要关心这个?通过这样做,我们可以使用我们模型的parameters()
方法来检索所有模型参数的迭代器,甚至是嵌套模型的那些参数,我们可以用它来提供我们的优化器(而不是自己构建参数列表!)。
此外,我们可以使用模型的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 的模型在行动——不再需要手动预测/前进
现在,打印出来的语句看起来像这样——参数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这样的机制,这些机制在训练和评估阶段具有不同的行为。
嵌套模型
在我们的模型中,我们手动创建了两个参数来执行线性回归。让我们使用 PyTorch 的线性模型作为我们自己的属性,从而创建一个嵌套模型。
尽管这显然是一个人为的例子,因为我们几乎没有添加任何有用的东西(或者根本没有!)来包装底层模型,它很好地说明了这个概念。
在该__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 的线性层构建模型
现在,如果我们调用parameters()
这个模型的方法,PyTorch 将以递归的方式计算其属性的参数。您可以使用类似的方法自己尝试:[*LayerLinearRegression().parameters()]
获取所有参数的列表。您还可以添加新Linear
属性,即使您在正向传递中根本不使用它们,它们仍会列在parameters()
下。
顺序模型
我们的模型非常简单……您可能会想:“为什么还要费心为它构建一个类?!” 好吧,你说得有道理……
对于使用普通层的简单模型,其中一个层的输出被顺序地作为输入提供给下一个,我们可以使用一个顺序模型。
在我们的例子中,我们将构建一个带有单个参数的顺序模型,即我们用来训练线性回归的Linear
层。该模型看起来像这样:
# Alternatively, you can use a Sequential model
model = nn.Sequential(nn.Linear(1, 1)).to(device)
很简单,对吧?
训练步骤
到目前为止,我们已经定义了一个优化器、一个损失函数和一个模型。向上滚动一点并快速查看循环内的代码。如果我们使用不同的优化器、损失函数甚至模型,它会改变吗?如果不是,我们怎样才能使它更通用?
好吧,我想我们可以说所有这些代码行都执行训练步骤,给定这三个元素(优化器、损失和模型)、特征和标签。
那么,如何编写一个接受这三个元素并返回另一个执行训练步骤的函数, 将一组特征和标签作为参数并返回相应的损失?
然后我们可以使用这个通用函数来构建一个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 张量。但是我们可以做得更好,我们可以建立一个……
数据集
在 PyTorch 中,数据集由继承自Dataset类的常规Python 类表示。您可以将其视为一种 Python元组列表,每个元组对应一个点 (features, label)。
它需要实现的最基本的方法是:
-
__init__(self)
:它接受构建元组列表所需的任何参数——它可能是将被加载和处理的 CSV 文件的名称;它可能是两个张量,一个用于特征,另一个用于标签;或其他任何东西,取决于手头的任务。
无需在构造方法 ( ) 中加载整个数据集__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 数组构建了训练张量,但我们没有将它们发送到设备?所以,它们现在是CPU张量!为什么?
我们不希望我们的整个训练数据都加载到 GPU 张量中,就像我们到目前为止在示例中所做的那样,因为它会占用我们宝贵的显卡 RAM 中的空间。
好的,很好,但话又说回来,我们为什么要构建数据集?我们这样做是因为我们想使用……
数据加载器
到目前为止,我们在每个训练步骤都使用了整个训练数据。一直都是批量梯度下降。这对于我们小得离谱的数据集来说很好,当然,但是如果我们想认真对待这一切,我们必须使用小批量梯度下降。因此,我们需要小批量。因此,我们需要相应地对数据集进行切片。你想手动做吗?!我也不!
所以我们使用 PyTorch 的DataLoader类来完成这项工作。我们告诉它要使用哪个数据集(我们刚刚在上一节中构建的数据集)、所需的小批量大小以及我们是否要对其进行洗牌。而已!
我们的加载器将表现得像一个迭代器,所以我们可以遍历它并每次获取不同的小批量。
from torch.utils.data import DataLoader
train_loader = DataLoader(dataset=train_data, batch_size=16, shuffle=True)
为我们的训练数据构建数据加载器
要检索样本小批量,只需运行下面的命令——它将返回一个包含两个张量的列表,一个用于特征,另一个用于标签。
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())
使用小批量梯度下降
现在有两点不同:不仅我们有一个内部循环从DataLoader
中来加载我们的每个小批量,而且更重要的是,我们现在只向设备发送一个小批量。
对于更大的数据集,使用Dataset的_
_get_item__
逐个样本加载数据 (进入CPU张量) ,然后将属于同一小批量的所有样本一次发送到您的 GPU(设备)是实现最佳使用的方法你的显卡的内存。
此外,如果你有很多 GPU来训练你的模型,最好让你的数据集“不可知”,并在训练期间将批次分配给不同的 GPU。
到目前为止,我们只关注训练 数据。我们为它构建了一个数据集和一个数据加载器。我们可以对验证数据做同样的事情,使用我们在本文开头执行的拆分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)
将数据集拆分为训练集和验证集,PyTorch 方式!现在我们有一个用于验证集的数据加载器,因此,将它用于评估是有意义的。
评估
这是我们旅程的最后一部分——我们需要更改训练循环以包括对我们模型的评估,即计算验证损失。第一步是包含另一个内部循环来处理来自验证加载器的小批量,将它们发送到与我们的模型相同的设备。接下来,我们使用我们的模型进行预测(第 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 的官方教程。
PyTorch官方教程链接:https://pytorch.org/tutorials/
推荐书单
《PyTorch深度学习简明实战 》
本书针对深度学习及开源框架——PyTorch,采用简明的语言进行知识的讲解,注重实战。全书分为4篇,共19章。深度学习基础篇(第1章~第6章)包括PyTorch简介与安装、机器学习基础与线性回归、张量与数据类型、分类问题与多层感知器、多层感知器模型与模型训练、梯度下降法、反向传播算法与内置优化器。计算机视觉篇(第7章~第14章)包括计算机视觉与卷积神经网络、卷积入门实例、图像读取与模型保存、多分类问题与卷积模型的优化、迁移学习与数据增强、经典网络模型与特征提取、图像定位基础、图像语义分割。自然语言处理和序列篇(第15章~第17章)包括文本分类与词嵌入、循环神经网络与一维卷积神经网络、序列预测实例。生成对抗网络和目标检测篇(第18章~第19章)包括生成对抗网络、目标检测。
本书适合人工智能行业的软件工程师、对人工智能感兴趣的学生学习,同时也可作为深度学习的培训教程。
作者简介:
日月光华:网易云课堂资深讲师,经验丰富的数据科学家和深度学习算法工程师。擅长使用Python编程,编写爬虫并利用Python进行数据分析和可视化。对机器学习和深度学习有深入理解,熟悉常见的深度学习框架( PyTorch、TensorFlow)和模型,有丰富的深度学习、数据分析和爬虫等开发经验,著有畅销书《Python网络爬虫实例教程(视频讲解版)》。
购买链接(新书限时5.5折):https://item.jd.com/13528847.html
精彩回顾
微信搜索关注《Python学研大本营》
访问【IT今日热榜】,发现每日技术热点