你应该知道的 PYTORCH 的 13 个特性

PyTorch 在学术界和工业应用研究中都获得了广泛的关注。它是一个深度学习框架,具有很大的弹性和大量的实用程序和功能,可以加快工作速度。PyTorch 的学习曲线不是那么陡峭,但在其中实现高效和干净的代码可能很棘手。在使用它超过 2 年之后,以下是我希望我在开始学习 PyTorch 时知道的最重要的 PyTorch 功能。

1. 数据集文件夹

人们在学习 PyTorch 时做的第一件事就是实现自己Dataset的某种类型。这是一个新手错误 - 没有必要浪费时间写这样的东西。通常,数据集是数据列表(或 numpy 数组)或磁盘上的文件。将数据组织在磁盘上总是更好,而不是编写自定义Dataset来加载某人(或您)决定用来存储它的奇怪格式。

分类器最常见的数据格式之一是拥有一个包含子文件夹的目录,代表类,以及这些子文件夹中的文件,代表示例,如下所示。

folder/class_0/file1.txt
folder/class_0/file2.txt
folder/class_0/...

folder/class_1/file3.txt
folder/class_1/file4.txt

folder/class_2/file5.txt
folder/class_2/...

有一种内置的方法可以加载这种数据集——无论你拥有的数据是图像、文本文件还是其他东西——只要使用DatasetFolder(https://pytorch.org/vision/0.8/datasets.html#datasetfolder)。令人惊讶的是,这个类是torchvision包的一部分,而不是核心 PyTorch。该课程非常全面 - 您可以从文件夹中过滤文件,使用自定义代码加载它们并动态转换原始文件。例子:

from torchvision.datasets import DatasetFolder
from pathlib import Path
# I have text files in this folder
ds = DatasetFolder("/Users/marcin/Dev/tmp/my_text_dataset", 
    loader=lambda path: Path(path).read_text(),
    extensions=(".txt",), #only load .txt files
    transform=lambda text: text[:100], # only take first 100 characters
)

# Everything you need is already there
len(ds), ds.classes, ds.class_to_idx
(20, ['novels', 'thrillers'], {'novels': 0, 'thrillers': 1})

如果您正在处理图像,还有一个torchvision.datasets.ImageFolder类,它基于DatasetLoader并且预先配置为加载图像。

2.停止使用.to(device)那么多:zeros_like/ones_like等。

我从各种 GitHub 存储库中阅读了很多 PyTorch 代码。最让我恼火的是,几乎在每个 repo 中都有很多*.to(device)行,它们将数据从 CPU 放到 GPU 或其他方式。这样的行通常出现在许多快速而肮脏的仓库或初学者教程中。我强烈鼓励尽可能少地实现此类操作,并依靠内置的 PyTorch 功能来自动执行此操作。.to(device)在这里和那里放置这些行通常会导致性能下降或 PyTorch 参与者最喜欢的例外:

Expected object of device type cuda but got device type cpu

显然,在某些情况下您无法绕过它,但涵盖了大多数(如果不是全部)琐碎的情况。示例之一是在自定义损失中可能需要的零/一/随机张量的初始化 - 由于深度神经网络的训练通常在 GPU 上进行,模型的输出已经在“cuda”设备上,但您现在需要有另一个零/一张量也在“cuda”设备上对其进行操作。这就是*_likePyTorch 操作派上用场的地方:

my_output # on any device, if it's cuda then my_zeros will also be on cuda
my_zeros = torch.zeros_like(my_output_from_model)

在幕后,PyTorch 所做的是调用以下操作:

my_zeros = torch.zeros(my_output.size(), dtype=my_output.dtype, layout=my_output.layout, device=my_output.device)

所以一切都设置正确,你减少了代码中出现错误的可能性。内置操作包括:

torch.zeros_like()
torch.ones_like()
torch.rand_like()
torch.randn_like()
torch.randint_like()
torch.empty_like()
torch.full_like()

3.注册缓冲区(又名nn.Module.register_buffer

这是我劝阻人们不要.to(device)到处使用的宣传活动的下一站。有时您的模型或损失函数需要预先设置并在forward调用 pass时使用的参数- 例如,它可以是一个“权重”参数,它可以缩放损失或一些不变的固定张量,但每次都使用它. 对于这种场景,使用nn.Module.register_buffer方法,它告诉 PyTorch 将您传递给它的值存储在模块中,并随模块移动这些值。如果您初始化您的模块,然后将其移动到 GPU,这些值也将自动移动。此外 - 如果您保存模块的状态 - 缓冲区也将被保存!

一旦注册,就可以forward像任何其他模块的属性一样在函数中访问这些值。

from torch import nn
import torch

class ModuleWithCustomValues(nn.Module):
    def __init__(self, weights, alpha):
        super().__init__()
        self.register_buffer("weights", torch.tensor(weights))
        self.register_buffer("alpha", torch.tensor(alpha))
    
    def forward(self, x):
        return x * self.weights + self.alpha

m = ModuleWithCustomValues(
    weights=[1.0, 2.0], alpha=1e-4
)
m(torch.tensor([1.23, 4.56]))
tensor([1.2301, 9.1201])

4. 内置 Identity()

有时,当您使用迁移学习时,您将需要用 1:1 映射替换某些层,这归结nn.Module为仅实现一个目的或返回输入值。PyTorch 内置了这个类:https://pytorch.org/docs/1.7.1/generated/torch.nn.Identity.html?highlight=identity#identity。

示例 - 您想从ResNet50分类层之前的预训练中获取图像表示。以下是如何执行此操作:

from torchvision.models import resnet50
model = resnet50(pretrained=True)
model.fc = nn.Identity()
last_layer_output = model(torch.rand((1, 3, 224, 224)))
last_layer_output.shape
torch.Size([1, 2048])

5.成对距离: torch.cdist

下次您将遇到计算两个张量之间的所有对欧几里得(或一般:p 范数)距离的问题时,请记住torch.cdist. 它正是这样做的,并且在使用欧几里德距离时还会自动使用矩阵乘法,从而提高性能。
文档:https://pytorch.org/docs/1.7.1/generated/torch.cdist.html#torch-cdist

points1 = torch.tensor([[0.0, 0.0], [1.0, 1.0], [2.0, 2.0]])
points2 = torch.tensor([[0.0, 0.0], [-1.0, -1.0], [-2.0, -2.0], [-3.0, -3.0]]) # batches don't have to be equal
torch.cdist(points1, points2, p=2.0)
tensor([[0.0000, 1.4142, 2.8284, 4.2426],
        [1.4142, 2.8284, 4.2426, 5.6569],
        [2.8284, 4.2426, 5.6569, 7.0711]])

不使用和使用矩阵乘法的性能 - 在我的机器上使用 mm 时速度提高了 2 倍以上。

%%timeit
points1 = torch.rand((512, 2))
points2 = torch.rand((512, 2))
torch.cdist(points1, points2, p=2.0, compute_mode="donot_use_mm_for_euclid_dist")

每个循环 867 µs ± 142 µs(7 次运行的平均值 ± 标准偏差,每次 1000 次循环)

%%timeit
points1 = torch.rand((512, 2))
points2 = torch.rand((512, 2))
torch.cdist(points1, points2, p=2.0)

每个循环 417 µs ± 52.9 µs(7 次运行的平均值 ± 标准偏差,每次 1000 次循环)

6.余弦相似度: F.cosine_similarity

保持与上一点相同的主题 - 计算距离 - 欧几里德距离并不总是您需要的东西。在处理向量时,通常选择余弦相似度作为度量。PyTorch 也有一个内置的余弦相似度实现。

import torch.nn.functional as F
vector1 = torch.tensor([0.0, 1.0])
vector2 = torch.tensor([0.05, 1.0])
print(F.cosine_similarity(vector1, vector2, dim=0))
vector3 = torch.tensor([0.0, -1.0])
print(F.cosine_similarity(vector1, vector3, dim=0))
tensor(0.9988)
tensor(-1.)

PyTorch 中的批处理余弦相似度

import torch.nn.functional as F
batch_of_vectors = torch.rand((4, 64))
similarity_matrix = F.cosine_similarity(batch_of_vectors.unsqueeze(1), batch_of_vectors.unsqueeze(0), dim=2)
similarity_matrix
tensor([[1.0000, 0.6922, 0.6480, 0.6789],
        [0.6922, 1.0000, 0.7143, 0.7172],
        [0.6480, 0.7143, 1.0000, 0.7312],
        [0.6789, 0.7172, 0.7312, 1.0000]])

7. 归一化向量: F.normalize

最后一点仍然与向量和距离松散连接的是归一化:通常通过改变向量的大小来提高计算的稳定性。最常用的归一化是 L2 规范化,可以在 PyTorch 中应用如下:

vector = torch.tensor([99.0, -512.0, 123.0, 0.1, 6.66])
normalized_vector = F.normalize(vector, p=2.0, dim=0)
normalized_vector
tensor([ 1.8476e-01, -9.5552e-01,  2.2955e-01,  1.8662e-04,  1.2429e-02])

在 PyTorch 中进行规范化的旧方法是:

vector = torch.tensor([99.0, -512.0, 123.0, 0.1, 6.66])
normalized_vector = vector / torch.norm(vector, p=2.0)
normalized_vector
tensor([ 1.8476e-01, -9.5552e-01,  2.2955e-01,  1.8662e-04,  1.2429e-02])

PyTorch 中的批量 L2 归一化

batch_of_vectors = torch.rand((4, 64))
normalized_batch_of_vectors = F.normalize(batch_of_vectors, p=2.0, dim=1)
normalized_batch_of_vectors.shape, torch.norm(normalized_batch_of_vectors, dim=1) # all vectors will have length of 1.0
(torch.Size([4, 64]), tensor([1.0000, 1.0000, 1.0000, 1.0000]))

8.线性层+分块技巧( torch.chunk)

这是我最近在这里发现的一个创意技巧。假设您想将输入映射到N 个不同的线性投影中。您可以通过创建做到这一点ñ nn.Linear层和做直传所有ň他们,或者你可以创建一个单一的线性层,做一个向前输出传递,只是块到ñ件。这种方法通常会带来更高的性能,所以记住这是一个很好的技巧。

d = 1024
batch = torch.rand((8, d))
layers = nn.Linear(d, 128, bias=False), nn.Linear(d, 128, bias=False), nn.Linear(d, 128, bias=False)
one_layer = nn.Linear(d, 128 * 3, bias=False)
%%timeit
o1 = layers[0](batch)
o2 = layers[1](batch)
o3 = layers[2](batch)

每个循环 289 µs ± 30.8 µs(7 次运行的平均值 ± 标准偏差,每次 1000 次循环)

%%timeit
o1, o2, o3 = torch.chunk(one_layer(batch), 3, dim=1)

每个循环 202 µs ± 8.09 µs(7 次运行的平均值 ± 标准偏差,每次 1000 次循环)

9. 蒙版选择 ( torch.masked_select)

有时您只需要对输入张量的某些部分进行计算。举个例子:你只想计算满足某些条件的张量的损失。为此,只需使用torch.masked_select- 请注意,当需要渐变时也可以使用此操作。

data = torch.rand((3, 3)).requires_grad_()
print(data)
mask = data > data.mean()
print(mask)
torch.masked_select(data, mask)
tensor([[0.0582, 0.7170, 0.7713],
        [0.9458, 0.2597, 0.6711],
        [0.2828, 0.2232, 0.1981]], requires_grad=True)
tensor([[False,  True,  True],
        [ True, False,  True],
        [False, False, False]])
tensor([0.7170, 0.7713, 0.9458, 0.6711], grad_fn=<MaskedSelectBackward>)

直接在张量上应用蒙版

通过使用掩码作为输入张量的“索引器”,可以实现类似的行为。

data[mask]
tensor([0.7170, 0.7713, 0.9458, 0.6711], grad_fn=<IndexBackward>)

有时,一个理想的解决方案是False用零填充掩码中的所有值,可以这样做:

data * mask
tensor([[0.0000, 0.7170, 0.7713],
        [0.9458, 0.0000, 0.6711],
        [0.0000, 0.0000, 0.0000]], grad_fn=<MulBackward0>)

10. 条件张量 torch.where

当您想将两个张量与条件组合时,此函数会派上用场 - 如果为真,则从第一个张量中获取元素,如果为假,则从第二个张量中获取。
文档:https://pytorch.org/docs/1.7.1/generated/torch.where.html#torch.where

x = torch.tensor([1.0, 2.0, 3.0, 4.0, 5.0], requires_grad=True)
y = -x
condition_or_mask = x <= 3.0
torch.where(condition_or_mask, x, y)
tensor([ 1.,  2.,  3., -4., -5.], grad_fn=<SWhereBackward>)

11. 用给定位置的值填充张量 ( Tensor.scatter)

此函数的用例如下 - 您想用给定索引处的另一个张量的值填充一个张量。一维张量更容易理解,因此我将首先展示它,然后继续进行更高级的示例。

data = torch.tensor([1, 2, 3, 4, 5])
index = torch.tensor([0, 1])
values = torch.tensor([-1, -2, -3, -4, -5])
data.scatter(0, index, values)
tensor([-1, -2,  3,  4,  5])

上面的例子很简单,但是现在看看如果你把 index 改为 index = torch.tensor([0, 1, 4])

data = torch.tensor([1, 2, 3, 4, 5])
index = torch.tensor([0, 1, 4])
values = torch.tensor([-1, -2, -3, -4, -5])
data.scatter(0, index, values)
tensor([-1, -2,  3,  4, -3])

为什么最后一个值是 now 是违反直觉的-3,对吗?这是 PyTorchscatter函数的主要部分。的index变量表示在哪个位置从data张量应在i-th从值values张量被放置。我希望下面这个操作的普通 python 等价物会更清楚:

data_orig = torch.tensor([1, 2, 3, 4, 5])
index = torch.tensor([0, 1, 4])
values = torch.tensor([-1, -2, -3, -4, -5])
scattered = data_orig.scatter(0, index, values)

data = data_orig.clone()
for idx_in_values, where_to_put_the_value in enumerate(index):
    what_value_to_put = values[idx_in_values]
    data[where_to_put_the_value] = what_value_to_put
data, scattered
(tensor([-1, -2,  3,  4, -3]), tensor([-1, -2,  3,  4, -3]))

二维数据上的 PyTorch 散点示例

永远记住, 的形状index与 的形状相关,其中的valuesindex对应于 中的位置data

data = torch.zeros((4, 4)).float()
index = torch.tensor([
    [0, 1],
    [2, 3],
    [0, 3],
    [1, 2]
])
values = torch.arange(1, 9).float().view(4, 2)
values, data.scatter(1, index, values)
(tensor([[1., 2.],
        [3., 4.],
        [5., 6.],
        [7., 8.]]),
tensor([[1., 2., 0., 0.],
        [0., 0., 3., 4.],
        [5., 0., 0., 6.],
        [0., 7., 8., 0.]]))

12. 网络内的图像插值 ( F.interpolate)

当我学习 PyTorch 时,令我惊讶的是您实际上可以在前向传递中调整图像(或任何中间张量)的大小并保持梯度流。这种方法在使用 CNN 和 GAN 时特别有用。

# image from https://commons.wikimedia.org/wiki/File:A_female_British_Shorthair_at_the_age_of_20_months.jpg
img = Image.open("./cat.jpg")
img
to_pil_image(
    F.interpolate(to_tensor(img).unsqueeze(0),  # batch of size 1
                  mode="bilinear", 
                  scale_factor=2.0, 
                  align_corners=False).squeeze(0) # remove batch dimension
)

PyTorch torchvision make_grid 教程

看看梯度流是如何保存的:

F.interpolate(to_tensor(img).unsqueeze(0).requires_grad_(),
                  mode="bicubic", 
                  scale_factor=2.0, 
                  align_corners=False)
tensor([[[[0.9216, 0.9216, 0.9216,  ..., 0.8361, 0.8272, 0.8219],
    [0.9214, 0.9214, 0.9214,  ..., 0.8361, 0.8272, 0.8219],
    [0.9212, 0.9212, 0.9212,  ..., 0.8361, 0.8272, 0.8219],
    ...,
    [0.9098, 0.9098, 0.9098,  ..., 0.3592, 0.3486, 0.3421],
    [0.9098, 0.9098, 0.9098,  ..., 0.3566, 0.3463, 0.3400],
    [0.9098, 0.9098, 0.9098,  ..., 0.3550, 0.3449, 0.3387]],

    [[0.6627, 0.6627, 0.6627,  ..., 0.5380, 0.5292, 0.5238],
    [0.6626, 0.6626, 0.6626,  ..., 0.5380, 0.5292, 0.5238],
    [0.6623, 0.6623, 0.6623,  ..., 0.5380, 0.5292, 0.5238],
    ...,
    [0.6196, 0.6196, 0.6196,  ..., 0.3631, 0.3525, 0.3461],
    [0.6196, 0.6196, 0.6196,  ..., 0.3605, 0.3502, 0.3439],
    [0.6196, 0.6196, 0.6196,  ..., 0.3589, 0.3488, 0.3426]],

    [[0.4353, 0.4353, 0.4353,  ..., 0.1913, 0.1835, 0.1787],
    [0.4352, 0.4352, 0.4352,  ..., 0.1913, 0.1835, 0.1787],
    [0.4349, 0.4349, 0.4349,  ..., 0.1913, 0.1835, 0.1787],
    ...,
    [0.3333, 0.3333, 0.3333,  ..., 0.3827, 0.3721, 0.3657],
    [0.3333, 0.3333, 0.3333,  ..., 0.3801, 0.3698, 0.3635],
    [0.3333, 0.3333, 0.3333,  ..., 0.3785, 0.3684, 0.3622]]]],
grad_fn=<UpsampleBicubic2DBackward1>)

13. 制作图像网格 ( torchvision.utils.make_grid)

在使用 PyTorch 和 torchvision 时,无需使用 matplotlib 或某些外部库复制粘贴代码以显示图像网格。就用torchvision.utils.make_grid
文档:https://pytorch.org/vision/0.8/utils.html

from torchvision.utils import make_grid
from torchvision.transforms.functional import to_tensor, to_pil_image
from PIL import Image
img = Image.open("./cat.jpg")
to_pil_image(
    make_grid(
        [to_tensor(i) for i in [img, img, img]],
         nrow=2, # number of images in single row
         padding=5 # "frame" size
     )
)

PyTorch torchvision make_grid 教程
Ref: https://zablo.net/blog/post/pytorch-13-features-you-should-know/

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

开源技术

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值