案例驱动,手把手教你学PyTorch(三)

通过案例学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

精彩回顾

《Pandas1.x实例精解》新书抢先看!

【第1篇】利用Pandas操作DataFrame的列与行

【第2篇】Pandas如何对DataFrame排序和统计

【第3篇】Pandas如何使用DataFrame方法链

【第4篇】Pandas如何比较缺失值以及转置方向?

【第5篇】DataFrame如何玩转多样性数据

【第6篇】如何进行探索性数据分析?

【第7篇】使用Pandas处理分类数据

【第8篇】使用Pandas处理连续数据

【第9篇】使用Pandas比较连续值和连续列

【第10篇】如何比较分类值以及使用Pandas分析库

微信搜索关注《Python学研大本营》

访问【IT今日热榜】,发现每日技术热点

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值