13個你一定要知道的PyTorch特性

編譯 | ronghuaiyang

PyTorch在學術界和工業界的應用研究中都獲得了很多關注。它是一個具有很大靈活性的深度學習框架,使用了大量的實用工具和函式來加快工作速度。PyTorch的學習曲線並不是那麼陡峭,但在其中實現高效和乾淨的程式碼可能會很棘手。在使用它超過2年之後,以下是我最喜歡的PyTorch功能,我希望我一開始學習它就知道。

1 DatasetFolder

當學習PyTorch時,人們首先要做的事情之一是實現自己的某種Dataset 。這是一個低階錯誤,沒有必要浪費時間寫這樣的東西。通常,資料集要麼是資料列表(或者是numpy陣列),要麼磁碟上的檔案。所以,把資料在磁碟上組織好,要比寫一個自定義的Dataset來載入某種奇怪的格式更好。

分類器最常見的資料格式之一,是有一個帶有子資料夾的目錄,子資料夾表示類,子資料夾中的檔案表示樣本,如下所示。

folder/class_0/file1。txtfolder/class_0/file2。txtfolder/class_0/。。。folder/class_1/file3。txtfolder/class_1/file4。txtfolder/class_2/file5。txtfolder/class_2/。。。

有一個內建的方式來載入這類資料集,不管你的資料是影象,文字檔案或其他什麼,只要使用‘DatasetFolder就可以了。令人驚訝的是,這個類是torchvision包的一部分,而不是核心PyTorch。這個類非常全面,你可以從資料夾中過濾檔案,使用自定義程式碼載入它們,並動態轉換原始檔案。例子:

from torchvision。datasets import DatasetFolderfrom pathlib import Path# I have text files in this folderds = 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 therelen(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 cudamy_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 nnimport torchclass 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。alpham = ModuleWithCustomValues( weights=[1。0, 2。0], alpha=1e-4)m(torch。tensor([1。23, 4。56]))tensor([1。2301, 9。1201])

4 Built-in Identity()

有時候,當你使用遷移學習時,你需要用1:1的對映替換一些層,可以用nn。Module來實現這個目的,只返回輸入值。PyTorch內建了這個類。

例子,你想要在分類層之前從一個預訓練過的ResNet50獲取影象表示。以下是如何做到這一點:

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

5 Pairwise distances: torch。cdist

下次當你遇到計算兩個張量之間的歐幾里得距離(或者一般來說:p範數)的問題時,請記住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 equaltorch。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倍以上。

%%timeitpoints1 = 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 per loop (mean±std。 dev。 of 7 run, 1000 loop each)

%%timeitpoints1 = torch。rand((512, 2))points2 = torch。rand((512, 2))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 Fvector1 = 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 Fbatch_of_vectors = torch。rand((4, 64))similarity_matrix = F。cosine_similarity(batch_of_vectors。unsqueeze(1), batch_of_vectors。unsqueeze(0), dim=2)similarity_matrixtensor([[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_vectortensor([ 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_vectortensor([ 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個不同的線性投影中。你可以透過建立N個nn。Linear來做到這一點。或者你也可以建立一個單一的線性層,做一個向前傳遞,然後將輸出分成N塊。這種方法通常會帶來更高的效能,所以這是一個值得記住的技巧。

d = 1024batch = 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)%%timeito1 = 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)

%%timeito1, 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((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=

直接在tensor上應用mask

類似的行為可以透過使用mask作為輸入張量的 “indexer”來實現。

data[mask]tensor([0。7170, 0。7713, 0。9458, 0。6711], grad_fn=

有時,一個理想的解決方案是用0填充mask中所有的False值,可以這樣做:

data * masktensor([[0。0000, 0。7170, 0。7713], [0。9458, 0。0000, 0。6711], [0。0000, 0。0000, 0。0000]], grad_fn=

10 使用 torch。where來對tensors加條件

當你想把兩個張量結合在一個條件下這個函式很有用,如果條件是真,那麼從第一個張量中取元素,如果條件是假,從第二個張量中取元素。

x = torch。tensor([1。0, 2。0, 3。0, 4。0, 5。0], requires_grad=True)y = -xcondition_or_mask = x <= 3。0torch。where(condition_or_mask, x, y)tensor([ 1。, 2。, 3。, -4。, -5。], grad_fn=

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

為什麼最後一個值是-3,這是反直覺的,對吧?這是PyTorch scatter函式的中心思想。index變量表示data張量的第i個值應該放在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_putdata, scattered(tensor([-1, -2, 3, 4, -3]), tensor([-1, -2, 3, 4, -3]))

2D資料的PyTorch scatter例子

始終記住,index的形狀與values的形狀相關,而index中的值對應於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和GANs時特別有用。

# image from https://commons。wikimedia。org/wiki/File:A_female_British_Shorthair_at_the_age_of_20_months。jpgimg = Image。open(“。/cat。jpg”)img

13個你一定要知道的PyTorch特性

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

13個你一定要知道的PyTorch特性

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

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=

13 將影象做成網格 (torchvision。utils。make_grid)

當使用PyTorch和torchvision時,不需要使用matplotlib或一些外部庫來複制貼上程式碼來顯示影象網格。只要使用torchvision。utils。make_grid就行了。

from torchvision。utils import make_gridfrom torchvision。transforms。functional import to_tensor, to_pil_imagefrom PIL import Imageimg = 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 ))

13個你一定要知道的PyTorch特性

原文連結:

https://zablo。net/blog/post/pytorch-13-features-you-should-know/