PyTorch的13个必知必会知识点

机器学习实验室

共 16933字,需浏览 34分钟

 · 2021-04-23


作者:MARCIN ZABŁOCKIMARCIN ZABŁOCKI

编译:ronghuaiyang

转自:AI公园


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

1. DatasetFolder

当学习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就可以了。令人惊讶的是,这个类是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转移到其他地方。这样的语句通常会出现在大量的repos或初学者教程中。我强烈建议尽可能少地实现这类操作,并依赖内置的PyTorch功能自动实现这类操作。到处使用.to(device)通常会导致性能下降,还会出现异常:

Expected object of device type cuda but got device type cpu

显然,有些情况下你无法回避它,但大多数情况(如果不是全部)都在这里。其中一种情况是初始化一个全0或全1的张量,这在深度神经网络计算损失的的时候是经常发生的,模型的输出已经在cuda上了,你需要另外的tensor也是在cuda上,这时,你可以使用*_like操作符:

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. Register Buffer ( nn.Module.register_buffer)

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

一旦注册,这些值就可以在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.02.0], alpha=1e-4
)
m(torch.tensor([1.234.56]))
tensor([1.23019.1201])

4. Built-in Identity()

有时候,当你使用迁移学习时,你需要用1:1的映射替换一些层,可以用nn.Module来实现这个目的,只返回输入值。PyTorch内置了这个类。

例子,你想要在分类层之前从一个预训练过的ResNet50获取图像表示。以下是如何做到这一点:

from torchvision.models import resnet50
model = resnet50(pretrained=True)
model.fc = nn.Identity()
last_layer_output = model(torch.rand((13224224)))
last_layer_output.shape
torch.Size([12048])

5. Pairwise distances: torch.cdist

下次当你遇到计算两个张量之间的欧几里得距离(或者一般来说:p范数)的问题时,请记住torch.cdist。它确实做到了这一点,并且在使用欧几里得距离时还自动使用矩阵乘法,从而提高了性能。

points1 = torch.tensor([[0.00.0], [1.01.0], [2.02.0]])
points2 = torch.tensor([[0.00.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.00001.41422.82844.2426],
        [1.41422.82844.24265.6569],
        [2.82844.24265.65697.0711]])

没有矩阵乘法或有矩阵乘法的性能,在我的机器上使用mm时,速度快了2倍以上。

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

867µs±142µs per loop (mean±std. dev. of 7 run, 1000 loop each)

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

417µs±52.9µs per loop (mean±std. dev. of 7 run, 1000 loop each)

6. Cosine similarity: F.cosine_similarity

与上一点相同,计算欧几里得距离并不总是你需要的东西。当处理向量时,通常余弦相似度是选择的度量。PyTorch也有一个内置的余弦相似度实现。

import torch.nn.functional as F
vector1 = torch.tensor([0.01.0])
vector2 = torch.tensor([0.051.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((464))
similarity_matrix = F.cosine_similarity(batch_of_vectors.unsqueeze(1), batch_of_vectors.unsqueeze(0), dim=2)
similarity_matrix
tensor([[1.00000.69220.64800.6789],
        [0.69221.00000.71430.7172],
        [0.64800.71431.00000.7312],
        [0.67890.71720.73121.0000]])

7. 归一化向量: F.normalize

最后一点仍然与向量和距离有松散的联系,那就是归一化:通常是通过改变向量的大小来提高计算的稳定性。最常用的归一化是L2,可以在PyTorch中按如下方式应用:

vector = torch.tensor([99.0-512.0123.00.16.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.0123.00.16.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((464))
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([464]), tensor([1.00001.00001.00001.0000]))

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

这是我最近发现的一个有创意的技巧。假设你想把你的输入映射到N个不同的线性投影中。你可以通过创建Nnn.Linear来做到这一点。或者你也可以创建一个单一的线性层,做一个向前传递,然后将输出分成N块。这种方法通常会带来更高的性能,所以这是一个值得记住的技巧。

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 per loop (mean ± std. dev. of 7 runs, 1000 loops each)

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

202 µs ± 8.09 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

9. Masked select (torch.masked_select)

有时你只需要对输入张量的一部分进行计算。给你一个例子:你想计算的损失只在满足某些条件的张量上。为了做到这一点,你可以使用torch.masked_select,注意,当需要梯度时也可以使用这个操作。

data = torch.rand((33)).requires_grad_()
print(data)
mask = data > data.mean()
print(mask)
torch.masked_select(data, mask)
tensor([[0.05820.71700.7713],
        [0.94580.25970.6711],
        [0.28280.22320.1981]], requires_grad=True)
tensor([[False,  True,  True],
        [ TrueFalse,  True],
        [FalseFalseFalse]])
tensor([0.71700.77130.94580.6711], grad_fn=<MaskedSelectBackward>)

直接在tensor上应用mask

类似的行为可以通过使用mask作为输入张量的 “indexer”来实现。

data[mask]
tensor([0.71700.77130.94580.6711], grad_fn=<IndexBackward>)

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

data * mask
tensor([[0.00000.71700.7713],
        [0.94580.00000.6711],
        [0.00000.00000.0000]], grad_fn=<MulBackward0>)

10. 使用 torch.where来对tensors加条件

当你想把两个张量结合在一个条件下这个函数很有用,如果条件是真,那么从第一个张量中取元素,如果条件是假,从第二个张量中取元素。

x = torch.tensor([1.02.03.04.05.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([12345])
index = torch.tensor([01])
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([12345])
index = torch.tensor([014])
values = torch.tensor([-1-2-3-4-5])
data.scatter(0, index, values)
tensor([-1-2,  3,  4-3])

为什么最后一个值是-3,这是反直觉的,对吧?这是PyTorch scatter函数的中心思想。index变量表示data张量的第i个值应该放在values张量的哪个位置。我希望下面的简单python版的这个操作能让你更明白:

data_orig = torch.tensor([12345])
index = torch.tensor([014])
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]))

2D数据的PyTorch scatter例子

始终记住,index的形状与values的形状相关,而index中的值对应于data中的位置。

data = torch.zeros((44)).float()
index = torch.tensor([
    [01],
    [23],
    [03],
    [12]
])
values = torch.arange(19).float().view(42)
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和GANs时特别有用。

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

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

F.interpolate(to_tensor(img).unsqueeze(0).requires_grad_(),
                  mode="bicubic"
                  scale_factor=2.0
                  align_corners=False)
tensor([[[[0.92160.92160.9216,  ..., 0.83610.82720.8219],
    [0.92140.92140.9214,  ..., 0.83610.82720.8219],
    [0.92120.92120.9212,  ..., 0.83610.82720.8219],
    ...,
    [0.90980.90980.9098,  ..., 0.35920.34860.3421],
    [0.90980.90980.9098,  ..., 0.35660.34630.3400],
    [0.90980.90980.9098,  ..., 0.35500.34490.3387]],

    [[0.66270.66270.6627,  ..., 0.53800.52920.5238],
    [0.66260.66260.6626,  ..., 0.53800.52920.5238],
    [0.66230.66230.6623,  ..., 0.53800.52920.5238],
    ...,
    [0.61960.61960.6196,  ..., 0.36310.35250.3461],
    [0.61960.61960.6196,  ..., 0.36050.35020.3439],
    [0.61960.61960.6196,  ..., 0.35890.34880.3426]],

    [[0.43530.43530.4353,  ..., 0.19130.18350.1787],
    [0.43520.43520.4352,  ..., 0.19130.18350.1787],
    [0.43490.43490.4349,  ..., 0.19130.18350.1787],
    ...,
    [0.33330.33330.3333,  ..., 0.38270.37210.3657],
    [0.33330.33330.3333,  ..., 0.38010.36980.3635],
    [0.33330.33330.3333,  ..., 0.37850.36840.3622]]]],
grad_fn=<UpsampleBicubic2DBackward1>)

13. 将图像做成网格 (torchvision.utils.make_grid)

当使用PyTorch和torchvision时,不需要使用matplotlib或一些外部库来复制粘贴代码来显示图像网格。只要使用torchvision.utils.make_grid就行了。

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




往期精彩:

【原创首发】机器学习公式推导与代码实现30讲.pdf

【原创首发】深度学习语义分割理论与实战指南.pdf

 谈中小企业算法岗面试

 算法工程师研发技能表

 真正想做算法的,不要害怕内卷

 算法工程师的日常,一定不能脱离产业实践

 技术学习不能眼高手低

 技术人要学会自我营销

 做人不能过拟合

求个在看

浏览 13
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报