Obsah
1. Manipulace s tenzory v knihovně PyTorch
2. Získání tvaru tenzoru (shape)
3. Způsob uložení tenzorů v paměti počítače nebo TPU
5. Specifikace či změna typu prvků tenzoru
6. Manipulace s objektem typu Storage
7. Způsob uložení prvků tenzoru druhého a třetího řádu
9. Specifikace záporných indexů a záporného kroku při provádění řezu
10. Operace řezu aplikovaná na matice (tenzory druhého řádu)
11. Výběr průsečíku řádků a sloupců jedinou operací řezu
12. Výběr průsečíků řádků a sloupců se specifikací kroku
13. Výběr je pouze pohledem (view) na původní tenzor
14. Druhý pohled na způsob uložení prvků tenzoru: atributy stride, storage_offset a is_contiguous
15. Atributy stride a storage_offset pro řezy vektorů
16. Atributy stride a storage_offset pro řezy matic
17. Změna tvaru tenzoru operací reshape
19. Repositář s demonstračními příklady
1. Manipulace s tenzory v knihovně PyTorch
V dnešním článku o knihovně PyTorch si vysvětlíme, jakým způsobem jsou tenzory uloženy v paměti počítače resp. alternativně v paměti GPU (nebo spíše TPU – Tensor Processing Unit). Se způsobem uložení do jisté míry souvisí i získání pohledu (view) na tenzor; příkladem je operace řezu (slice). A následně si popíšeme i různé operace, které je možné s tenzory provádět jako s celkem. V této oblasti se jedná především o operaci změny tvaru tenzoru (shape); tato operace se tedy logicky jmenuje reshape.
2. Získání tvaru tenzoru (shape)
Tvar tenzoru neboli shape je popsán n-ticí obsahující počet prvků v jednotlivých dimenzích (ovšem pokud se tvar tenzoru čte, získáme hodnotu typu torch.Size, v níž je n-tice zabalená). Poněkud speciálním případem je skalár (vektor nultého řádu), který je popsán prázdnou n-ticí. Vektor je popsán n-ticí s jednou hodnotou – délkou vektoru, matice n-ticí se dvěma hodnotami atd. atd. Můžeme si to velmi snadno otestovat:
import torch # konstrukce tenzoru + zjisteni jejich tvaru # tenzor nulteho radu - skalar s1 = torch.tensor(100) print(s1.shape) # tenzor prvniho radu - vektor v1 = torch.Tensor(1) print(v1.shape) v2 = torch.Tensor(3) print(v2.shape) # tenzor druheho radu - matice m1 = torch.Tensor(3, 4) print(m1.shape) # tenzor tretiho radu - 3D pole c1 = torch.Tensor(3, 4, 5) print(c1.shape)
Po spuštění tohoto demonstračního příkladu by se měly zobrazit následující tvary:
torch.Size([]) torch.Size([1]) torch.Size([3]) torch.Size([3, 4]) torch.Size([3, 4, 5])
Specifikaci tvaru tenzoru založenou na sekvenci celých čísel jsme využili například v konstruktoru zeros:
# konstrukce tenzoru prvniho radu, vyplneni nulami v1 = torch.zeros(1) print(v1) print() # konstrukce tenzoru prvniho radu, vyplneni nulami v2 = torch.zeros(10) print(v2) print() # konstrukce tenzoru druheho radu, vyplneni nulami m1 = torch.zeros(3, 4) print(m1) print() # konstrukce tenzoru tretiho radu, vyplneni nulami c1 = torch.zeros(3, 4, 5) print(c1) print()
Výsledky:
tensor([0.]) tensor([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]) tensor([[0., 0., 0., 0.], [0., 0., 0., 0.], [0., 0., 0., 0.]]) tensor([[[0., 0., 0., 0., 0.], [0., 0., 0., 0., 0.], [0., 0., 0., 0., 0.], [0., 0., 0., 0., 0.]], [[0., 0., 0., 0., 0.], [0., 0., 0., 0., 0.], [0., 0., 0., 0., 0.], [0., 0., 0., 0., 0.]], [[0., 0., 0., 0., 0.], [0., 0., 0., 0., 0.], [0., 0., 0., 0., 0.], [0., 0., 0., 0., 0.]]])
Dtto pro konstruktor ones:
import torch # konstrukce tenzoru, vyplneni jednickami v1 = torch.ones(1) print(v1) print() # konstrukce tenzoru prvniho radu, vyplneni jednickami v2 = torch.ones(10) print(v2) print() # konstrukce tenzoru druheho radu, vyplneni jednickami m1 = torch.ones(3, 4) print(m1) print() # konstrukce tenzoru tretiho radu, vyplneni jednickami c1 = torch.ones(3, 4, 5) print(c1) print()
Výsledky:
tensor([1.]) tensor([1., 1., 1., 1., 1., 1., 1., 1., 1., 1.]) tensor([[1., 1., 1., 1.], [1., 1., 1., 1.], [1., 1., 1., 1.]]) tensor([[[1., 1., 1., 1., 1.], [1., 1., 1., 1., 1.], [1., 1., 1., 1., 1.], [1., 1., 1., 1., 1.]], [[1., 1., 1., 1., 1.], [1., 1., 1., 1., 1.], [1., 1., 1., 1., 1.], [1., 1., 1., 1., 1.]], [[1., 1., 1., 1., 1.], [1., 1., 1., 1., 1.], [1., 1., 1., 1., 1.], [1., 1., 1., 1., 1.]]])
3. Způsob uložení tenzorů v paměti počítače nebo TPU
V úvodním článku jsme si řekli, že objekty typu Tensor pro uložení svých komponent (prvků) interně používají jednorozměrná pole. To, že se tenzory uživateli jeví jako 1D, 2D, 3D atd. struktury je ve skutečnosti záležitostí pohledů (views) na zmíněná jednorozměrná pole (jedná se vlastně o běžná céčková pole). Pro některé nízkoúrovňové operace může být výhodné přistupovat přímo k internímu poli, což nám knihovna PyTorch umožňuje, protože nabízí objekty typu Storage. Existuje přitom několik konkrétních typů Storage, protože interní (céčková) pole taktéž mohou obsahovat prvky různých typů. Implicitně se používá FloatStorage, pokud není z typu prvků tenzorů zřejmé, že se má jednat o jiný typ (v budoucnu ovšem takto nízkoúrovňový přístup již nebude možný, o čemž se ještě zmíníme v dalších článcích):
Typ Storage | Význam |
---|---|
torch.BoolStorage | pole s prvky typu boolean |
torch.CharStorage | pole s prvky typu char |
torch.ByteStorage | pole s prvky typu unsigned char |
torch.ShortStorage | pole s prvky typu short (16bitové celé číslo) |
torch.IntStorage | pole s prvky typu int (32bitové celé číslo) |
torch.LongStorage | pole s prvky typu long (64bitové celé číslo) |
torch.BFloat16Storage | pole s prvky typu brain floating point/Bfloat16 |
torch.HalfStorage | pole s prvky typu half dle IEEE 754 |
torch.FloatStorage | pole s prvky typu float dle IEEE 754 |
torch.DoubleStorage | pole s prvky typu double dle IEEE 754 |
torch.ComplexFloatStorage | komplexní hodnoty založené na typu float |
torch.ComplexDoubleStorage | komplexní hodnoty založené na typu double |
torch.QUInt8Storage | kvantované hodnoty interně uložené do bajtu |
torch.QInt8Storage | kvantované hodnoty interně uložené do bajtu |
torch.QUInt2×4Storage | kvantované hodnoty interně uložené do bajtu |
torch.QUInt4×2Storage | kvantované hodnoty interně uložené do bajtu |
torch.QInt32Storage | kvantované hodnoty interně uložené do 32bitového slova |
torch.UntypedStorage | bez specifikace typu (jen s tímto typem se setkáme v budoucnu) |
4. Získání typu prvků tenzoru
Ještě než se podíváme na to, jakým způsobem je možné zjistit interní pole, které reprezentuje tenzor v paměti, si ukažme způsob zjištění typů prvku tenzoru. Bude se tedy jednat o informaci důležitou při „vysokoúrovňovém“ pohledu na tenzor, kdy nás nezajímají implementační detaily. Pro získání, jakého typu je každý prvek tenzoru, stačí přečíst atribut nazvaný dtype:
import torch # konstrukce tenzoru s inicializaci prvku v1 = torch.tensor(10) print(v1) print(type(v1)) print(v1.dtype) print() # konstrukce tenzoru bez inicializace prvku m1 = torch.Tensor(3, 4) print(m1) print(type(m1)) print(m1.dtype)
V prvním případě (jedná se o tenzor prvního řádu s jedním prvkem) je typ tohoto prvku nastaven na int64, což odpovídá předané hodnotě. Na druhou stranu by možná bylo možné použít i typ s menší bitovou šířkou:
tensor(10) <class 'torch.Tensor'> torch.int64
Ve druhém případě (tenzor druhého řádu, neboli v našem podání běžná 2D matice) je, možná poněkud překvapivě, použit typ float32 resp. přesněji řečeno torch.float32 (a prvky tenzoru nejsou inicializovány, takže mohou obsahovat náhodné hodnoty):
tensor([[-2.4020e-35, 4.5600e-41, -2.4020e-35, 4.5600e-41], [ 0.0000e+00, 0.0000e+00, 0.0000e+00, 0.0000e+00], [ 1.5520e-33, 2.2060e-38, -2.2407e+29, 4.8069e-07]]) <class 'torch.Tensor'> torch.float32
5. Specifikace či změna typu prvků tenzoru
Typy, které mají mít všechny prvky tenzoru, lze specifikovat již při jeho konstrukci. Ukažme si to na jednoduchém příkladu s tenzorem nultého řádu neboli skalárem. Budeme chtít, aby prvek byl typu bfloat16 (viz též tento článek):
# konstrukce tenzoru nulteho radu (skalaru) v1 = torch.tensor(10, dtype=torch.bfloat16) print(v1) print(type(v1)) print(v1.dtype)
Výsledný tenzor, který získáme, by měl vypadat následovně:
tensor(10., dtype=torch.bfloat16) torch.bfloat16
Ovšem v případě, že již máme k dispozici zkonstruovaný tenzor, znamená změna typu jeho prvků vlastně vytvoření nového tenzoru (opět – buď v paměti počítače nebo TPU). Tato konverze tenzorů se provádí metodou type, které předáme požadovaný typ prvků nově vytvořeného tenzoru:
# konstrukce tenzoru prvniho radu v2 = torch.arange(10, 20).type(torch.bfloat16) print(v2) print(type(v2)) print(v2.dtype) print() # konstrukce tenzoru druheho radu se zmenou typu prvku m1 = torch.Tensor(3, 4).type(torch.bfloat16) print(m1) print(type(m1)) print(m1.dtype)
Z výsledků je patrné, že se konverze skutečně podařila a že v prvním případě došlo ke konverzi hodnot (ve druhém případě taktéž, ovšem tenzor nebyl naplněn žádnými daty):
tensor([10., 11., 12., 13., 14., 15., 16., 17., 18., 19.], dtype=torch.bfloat16) <class 'torch.Tensor'> torch.bfloat16 tensor([[ 1.6213e+19, 0.0000e+00, 1.6213e+19, 0.0000e+00], [ 1.7090e-02, 8.0889e-32, 1.1581e-22, -1.0402e+10], [ 8.3200e-32, 1.8529e-21, 3.8738e+20, 1.6941e-20]], dtype=torch.bfloat16) <class 'torch.Tensor'> torch.bfloat16
6. Manipulace s objektem typu Storage
Ukažme si nyní, jak je možné pro existující tenzor získat objekt typu Storage a jak se dá tento objekt využít pro změnu hodnot komponent tenzoru. Nejprve vytvoříme běžný vektor s deseti prvky:
v1 = torch.tensor([1, 255, 65535, 65536]) print(v1) print(type(v1)) print(v1.dtype)
Vypíše se:
tensor([ 1, 255, 65535, 65536]) <class 'torch.Tensor'> torch.int64
Následně pro tento vektor získáme objekt typu Storage a vypíšeme si jeho obsah (tedy data tvořící zdroj pro samotný tenzor):
# informace o typu Storage storage = v1.untyped_storage() print(storage) print(type(storage))
Z vypsaných informací je patrné (resp. můžeme relativně snadno odvodit), že je tenzor uložen v paměti počítače (a ne TPU) i způsob uložení jeho čtyř prvků:
1 0 0 0 0 0 0 0 255 0 0 0 0 0 0 0 255 255 0 0 0 0 0 0 0 0 1 0 0 0 0 0 [torch.storage.UntypedStorage(device=cpu) of size 32] <class 'torch.storage.UntypedStorage'>
Jednotlivé prvky (rozepsané na bajty) jsou tedy uloženy takto:
1 0 0 0 0 0 0 0 = 1 255 0 0 0 0 0 0 0 = 255 255 255 0 0 0 0 0 0 = 255*256 + 255 = 65535 0 0 1 0 0 0 0 0 = 1*256*256 = 65536
7. Způsob uložení prvků tenzoru druhého a třetího řádu
Zajímavé (a později i užitečné) bude zjištění, jakým způsobem jsou interně uloženy prvky tenzorů druhého a třetího řádu. Nejprve si ukažme tenzory druhého řádu, které můžeme pro naše potřeby považovat za běžné matice. Zde se nabízí dva možné způsoby uložení – po řádcích nebo po sloupcích. Vytvořme si tedy jednoduchý tenzor pouze se šesti prvky. Navíc budeme ukládat pouze celá osmibitová čísla, což nám čtení výsledků ještě více zjednoduší:
import torch # konstrukce tenzoru druheho radu m2 = torch.tensor([[1,2,3], [4,5,6]]).type(torch.uint8) print(m2) print(type(m2)) print(m2.dtype) print() # informace o typu Storage storage = m2.untyped_storage() print(storage) print(type(storage))
Tenzor je nejdříve vypsán ve své původní podobě:
tensor([[1, 2, 3], [4, 5, 6]], dtype=torch.uint8) <class 'torch.Tensor'> torch.uint8
Na dalších řádcích výstupu se zobrazí interní pohled na storage, ze kterého je vidět uspořádání po řádcích (tedy „podle céčka“, nikoli „podle Fortanu“):
1 2 3 4 5 6 [torch.storage.UntypedStorage(device=cpu) of size 6] <class 'torch.storage.UntypedStorage'>
Podobně můžeme postupovat pro tenzory třetího řádu, tedy z našeho pohledu pro 3D pole:
import torch # konstrukce tenzoru tretiho radu c1 = torch.tensor([[[1,2,3], [4,5,6]], [[7,8,9], [10,11,12]]]).type(torch.int16) print(c1) print(type(c1)) print(c1.dtype) print() # informace o typu Storage storage = c1.untyped_storage() print(storage) print(type(storage))
Výpis obsahu tenzoru:
tensor([[[ 1, 2, 3], [ 4, 5, 6]], [[ 7, 8, 9], [10, 11, 12]]], dtype=torch.int16) <class 'torch.Tensor'> torch.int16
Interní uložení prvků tenzoru:
1 0 2 0 3 0 4 0 5 0 6 0 7 0 8 0 9 0 10 0 11 0 12 0 [torch.storage.UntypedStorage(device=cpu) of size 24] <class 'torch.storage.UntypedStorage'>
Každý prvek je v tomto případě uložen v šestnácti bitech (tj. na osmi bajtech), takže se vlastně jedná o následující pole:
1 0 2 0 3 0 4 0 5 0 6 0 7 0 8 0 9 0 10 0 11 0 12 0
Z výše uvedeného je patrné, že jsou prvky ukládány v pořadí od nejnižší dimenze k dimenzi nejvyšší. Tomu je nutné přizpůsobit i algoritmy.
8. Konstrukce řezu (slice)
Další zajímavou a velmi užitečnou operací, která produkuje pohled (view) nad tenzorem, je operace nazvaná slice (původně sub. Ta umožňuje z tenzoru získat určitou část, s níž bude možné pracovat jako s dalším tenzorem. Nejprve se podíváme na nejjednodušší použití této operace při práci s vektory. Vytvoříme si vektor s deseti prvky od 1/1 do 1/10:
# konstrukce tenzoru v1 = torch.range(10, 20) print(v1)
Vektor skutečně obsahuje deset prvků:
tensor([10, 11, 12, 13, 14, 15, 16, 17, 18, 19])
Pomocí operace slice můžeme získat pohled na prvky ležícími mezi specifikovaným dolním a horním indexem. Třetí až osmý prvek se tedy přečte takto:
# konstrukce rezu z tenzoru prvniho radu v2 = v1[2:8] print(v2)
Povšimněte si, že výsledkem operace slice je opět plnohodnotný tenzor.
tensor([12, 13, 14, 15, 16, 17])
Můžeme specifikovat i krok, tedy vlastně rozdíl v indexech prvků z původního tenzoru:
# konstrukce rezu z tenzoru prvniho radu v3 = v1[2:8:2] print(v3)
S výsledkem:
tensor([12, 14, 16])
9. Specifikace záporných indexů a záporného kroku při provádění řezu
Při zadávání indexů prvků je možné použít i záporné hodnoty. Jak je zvykem, jsou tyto hodnoty chápány jako indexy od konce vektoru. Další příklad nám tedy dá stejný výsledek, jako příklad předchozí (druhý prvek od začátku až druhý prvek od konce; povšimněte si, proč by v praxi bylo více konzistentní pracovat s indexy začínajícími od 1 a nikoli od 0):
# konstrukce rezu z tenzoru prvniho radu v4 = v1[-8:-2] print(v4)
Výsledek:
tensor([12, 13, 14, 15, 16, 17])
Horní nebo dolní index lze vynechat. Za dolní index se v tomto případě dosadí nula a za horní index počet prvků tenzoru:
# konstrukce rezu z tenzoru prvniho radu v5 = v1[:-2] print(v5) # konstrukce rezu z tenzoru prvniho radu v6 = v1[-3:] print(v6)
Nyní budou výsledky vypadat následovně:
tensor([10, 11, 12, 13, 14, 15, 16, 17]) tensor([17, 18, 19])
V případě, že je dolní index větší než index horní, vrátí se prázdný tenzor (což je zcela legitimní datová struktura):
# konstrukce rezu z tenzoru prvniho radu v7 = v1[10:0] print(v7)
Výsledný tenzor:
tensor([], dtype=torch.int64)
Mohlo by se zdát, že se PyTorch v případě řezů chová stejně, jako knihovna NumPy. Ovšem v případě záporného kroku tomu tak není:
# konstrukce rezu z tenzoru prvniho radu v8 = v1[10:1:-1] print(v8)
Nyní dojde k vyhození výjimky:
Traceback (most recent call last): File "/home/ptisnovs/xy/src/tensor_slice_operation_1.py", line 32, in <module< v8 = v1[10:1:-1] ~~^^^^^^^^^ ValueError: step must be greater than zero
Proč tomu tak je, si vysvětlíme v navazujícím textu.
10. Operace řezu aplikovaná na matice (tenzory druhého řádu)
Operaci slice je samozřejmě možné aplikovat i na matice a tenzory vyšších řádů; výsledkem je vždy opět tenzor. Nejprve si pro ukázku vytvoříme 2D matici a naplníme ji prvky s hodnotami 1 až 16:
# konstrukce tenzoru m1 = torch.Tensor([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], [13, 14, 15, 16]]) print(m1)
Matice se skutečně vytvořila:
tensor([[ 1., 2., 3., 4.], [ 5., 6., 7., 8.], [ 9., 10., 11., 12.], [13., 14., 15., 16.]])
Pokusme se nyní vytvořit řez obsahující řádky 1 a 2 (vyšší index není do výsledku zahrnut):
# konstrukce rezu - pres radky m2 = m1[1:3] print(m2)
Výsledný tenzor by měl vypadat následovně:
tensor([[ 5., 6., 7., 8.], [ 9., 10., 11., 12.]])
To ovšem není zdaleka vše, protože je možné specifikovat indexy i ve druhé dimenzi. Následující příkaz tedy získá tenzor tvořený původním sloupcem číslo 1 a 2 (indexuje se od nuly):
# konstrukce rezu - pres sloupce m3 = m1[:,1:3] print(m3)
Přesvědčme se, že tomu tak skutečně je:
tensor([[ 2., 3.], [ 6., 7.], [10., 11.], [14., 15.]])
import torch # konstrukce tenzoru m1 = torch.Tensor([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], [13, 14, 15, 16]]) print(m1) print() # konstrukce rezu - pres radky m2 = m1[-3:-1] print(m2) print() # konstrukce rezu - pres sloupce m3 = m1[:,-3:-1] print(m3)
Výsledky získané po spuštění tohoto skriptu:
tensor([[ 1., 2., 3., 4.], [ 5., 6., 7., 8.], [ 9., 10., 11., 12.], [13., 14., 15., 16.]]) tensor([[ 5., 6., 7., 8.], [ 9., 10., 11., 12.]]) tensor([[ 2., 3.], [ 6., 7.], [10., 11.], [14., 15.]])
11. Výběr průsečíku řádků a sloupců jedinou operací řezu
Operace pro výběr celých řádků nebo sloupců, kterou jsme si ukázali v předchozí kapitole, je dokonce možné zkombinovat a vybrat z tenzoru druhého řádu (zde se to ukazuje nejlépe) průsečík několika řádků a sloupců. Výsledkem většinou opět bude tenzor druhého řádu.
Opět si to ukažme na tenzoru, který reprezentuje matici 4×4 prvky:
# konstrukce tenzoru m1 = torch.Tensor([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], [13, 14, 15, 16]]) print(m1) print()
Tento tenzor je zobrazen následujícím způsobem (což by ostatně pro nás nemělo být nic nového):
tensor([[ 1., 2., 3., 4.], [ 5., 6., 7., 8.], [ 9., 10., 11., 12.], [13., 14., 15., 16.]])
Nyní se můžeme pokusit vybrat podmatici tvořenou průsečíkem druhého a třetího řádku a současně druhého a třetího sloupce. Tuto operaci zapíšeme následujícím způsobem:
# konstrukce rezu - pres radky i sloupce m2 = m1[1:3, 1:3] print(m2)
Výsledkem bude matice 2×2 prvky:
tensor([[ 6., 7.], [10., 11.]])
Opět pochopitelně můžeme při specifikaci indexů vynechat první nebo poslední index:
# konstrukce rezu - pres radky i sloupce m3 = m1[:3, :3] print(m3) print() # konstrukce rezu - pres radky i sloupce m4 = m1[1:, 1:] print(m4)
Výsledky těchto dvou operací budou podmatice 3×3 prvky. Každá z těchto matic bude pochopitelně odlišná:
tensor([[ 1., 2., 3.], [ 5., 6., 7.], [ 9., 10., 11.]]) tensor([[ 6., 7., 8.], [10., 11., 12.], [14., 15., 16.]])
12. Výběr průsečíků řádků a sloupců se specifikací kroku
I při výběru podmatice tvořené průsečíky řádků a sloupců můžeme specifikovat krok, tedy offset mezi řádky a/nebo sloupci. Implicitní hodnota kroku je (podle očekávání) rovna jedné, ovšem můžeme například zvolit krok 2. Opět si to ukážeme na jednoduchém příkladu. Začneme maticí 4×4 prvky, kterou již dobře známe:
# konstrukce tenzoru m1 = torch.Tensor([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], [13, 14, 15, 16]]) print(m1)
Výběr každého druhého sloupce a každého druhého řádku s vrácením jen těch prvků, které leží na průsečících:
# konstrukce rezu - pres radky i sloupce m2 = m1[::2, ::2] print(m2)
Výsledkem je opět matice o velikosti 2×2, tentokrát se však striktně řečeno nejedná o podmatici:
tensor([[ 1., 3.], [ 9., 11.]])
Podobný výběr, tentokrát však začneme na druhém sloupci a druhém řádku:
# konstrukce rezu - pres radky i sloupce m3 = m1[1::2, 1::2] print(m3)
Výsledkem bude odlišná matice o velikosti 2×2 prvky:
tensor([[ 6., 8.], [14., 16.]])
13. Výběr je pouze pohledem (view) na původní tenzor
V předchozím textu jsem napsal, že výsledkem řezu aplikovaného na tenzor je jiný tenzor. To je sice pravda, ovšem oba tenzory budou sdílet svoje data (storage). Co to ovšem znamená v praxi? Pokud budeme tenzory pouze číst, nepoznáme rozdíl, ovšem zápis do jednoho z těchto tenzorů povede k tomu, že se změní i prvky ve druhém (či prvním) tenzoru. Ukažme si to na příkladu tenzoru prvního řádu – vektoru:
# konstrukce tenzoru v1 = torch.arange(10, 20) print(v1)
Získáme tento vektor:
tensor([10, 11, 12, 13, 14, 15, 16, 17, 18, 19])
Z tohoto vektoru si přečteme řez:
# konstrukce rezu v2 = v1[5:10] print(v2)
Řez vypadá jako nový nezávislý tenzor:
tensor([15, 16, 17, 18, 19])
Do původního vektoru provedeme zápis – změníme jeho prvek:
# modifikace vektoru v1[5] = 999 print(v1) print(v2)
Z výpisů je patrné, že se změnil jak původní vektor, tak i řez:
tensor([ 10, 11, 12, 13, 14, 999, 16, 17, 18, 19]) tensor([999, 16, 17, 18, 19])
Nyní naopak budeme modifikovat řez:
# modifikace rezu v2[0] = 0 print(v1) print(v2)
Podle očekávání se v tomto případě opět změní jak řez (nový tenzor), tak i původní vektor (tenzor):
tensor([10, 11, 12, 13, 14, 0, 16, 17, 18, 19]) tensor([ 0, 16, 17, 18, 19])
Ovšem totéž chování lze pozorovat i u řezů maticemi popř. tenzory ještě vyšších řádů. Nyní tedy jen v krátkosti:
import torch # konstrukce tenzoru m1 = torch.Tensor([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], [13, 14, 15, 16]]) print(m1) print() # konstrukce rezu - pres radky i sloupce m2 = m1[1:3, 1:3] print(m2) print() # modifikace rezu m2[0,0] = 99 m2[0,1] = 99 m2[1,0] = 99 m2[1,1] = 99 print(m2) print() # vypis puvodniho tenzoru print(m1)
Výsledky (původní tenzor – matice – je zobrazen na posledním místě):
tensor([[ 1., 2., 3., 4.], [ 5., 6., 7., 8.], [ 9., 10., 11., 12.], [13., 14., 15., 16.]]) tensor([[ 6., 7.], [10., 11.]]) tensor([[99., 99.], [99., 99.]]) tensor([[ 1., 2., 3., 4.], [ 5., 99., 99., 8.], [ 9., 99., 99., 12.], [13., 14., 15., 16.]])
A modifikace řezů vytvořených na základě kroku, který je vyšší než jeho implicitní hodnota (1):
import torch # konstrukce tenzoru m1 = torch.Tensor([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], [13, 14, 15, 16]]) print(m1) print() # konstrukce rezu - pres radky i sloupce m2 = m1[::2, ::2] print(m2) print() # konstrukce rezu - pres radky i sloupce m3 = m1[1::2, 1::2] print(m3) print() # modifikace rezu m2[0,0] = 99 m2[0,1] = 99 m2[1,0] = 99 m2[1,1] = 99 print(m2) print() # vypis puvodniho tenzoru print(m1) print() # modifikace rezu m3[0,0] = -99 m3[0,1] = -99 m3[1,0] = -99 m3[1,1] = -99 print(m3) print() # vypis puvodniho tenzoru print(m1)
Z výsledků je patrné, jak se původní matice mění, a to nepřímo – modifikacemi jejích řezů:
tensor([[ 1., 2., 3., 4.], [ 5., 6., 7., 8.], [ 9., 10., 11., 12.], [13., 14., 15., 16.]]) tensor([[ 1., 3.], [ 9., 11.]]) tensor([[ 6., 8.], [14., 16.]]) tensor([[99., 99.], [99., 99.]]) tensor([[99., 2., 99., 4.], [ 5., 6., 7., 8.], [99., 10., 99., 12.], [13., 14., 15., 16.]]) tensor([[-99., -99.], [-99., -99.]]) tensor([[ 99., 2., 99., 4.], [ 5., -99., 7., -99.], [ 99., 10., 99., 12.], [ 13., -99., 15., -99.]])
14. Druhý pohled na způsob uložení prvků tenzoru: atributy stride, storage_offset a is_contiguous
V prvním článku o knihovně PyTorch jsme si řekli, že tenzory vytvářené základními konstruktory, mj. tenzor s nulovými komponentami, tenzor s komponentami nastavenými na jedničku, vektor vytvořený konstruktorem range či tenzor vytvořený z tabulek (polí) jazyka Python, jsou v operační paměti uloženy stejným způsobem, jako klasická pole v programovacím jazyku C. Ovšem některé popsané operace (například operace řezu) mohou vracet jen vybrané komponenty, které již v obecných případech nemusí v operační paměti ležet kontinuálně za sebou. To sice z pohledu uživatele nepředstavuje větší problém (s výsledkem se stále pracuje jako s plnohodnotným tenzorem), ovšem některé operace se mohou provádět pomaleji, zejména u rozsáhlejších tenzorů. Mj. i z tohoto důvodu byla do knihovny PyTorch přidána metoda is_contiguous(), která zjišťuje, zda jsou prvky tenzoru (či pohledu na něj) uloženy za sebou či nikoli.
Několik dalších metod pak slouží k získání základních informací o daném tenzoru. Především lze zjistit velikost tenzoru, počet dimenzí, počet elementů, offset mezi sousedními komponentami, řádky, maticemi … (stride) a taktéž offset první komponenty v rámci „pohledu“ (storageOffset).
Zajímavé bude zjistit základní vlastnosti tenzorů různých řádů, které ovšem vznikly tak, že jsou v nich prvky uloženy kontinuálně za sebou:
import torch # konstrukce tenzoru v1 = torch.arange(10, 20) print("v1:") print(v1) print("Stride:", v1.stride()) print("Offset:", v1.storage_offset()) print("Contiguous:", v1.is_contiguous()) print() # konstrukce tenzoru m1 = torch.Tensor([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], [13, 14, 15, 16]]) print("m1:") print(m1) print("Stride:", m1.stride()) print("Offset:", m1.storage_offset()) print("Contiguous:", m1.is_contiguous()) print() # 3D struktura m3 = [[[1, 2, 3], [4, 5, 6], [7, 8, 9]], [[10, 20, 30], [40, 50, 60], [70, 80, 90]]] # konstrukce tenzoru tretiho radu c1 = torch.Tensor(m3) print("c1:") print(c1) print("Stride:", c1.stride()) print("Offset:", c1.storage_offset()) print("Contiguous:", c1.is_contiguous())
Všechny tyto tenzory mají Contiguous==True a nulový offset. Hodnota stride odpovídá rozdílům offsetů prvků v jednotlivých dimenzích (po sobě, mezi řádky, mezi maticemi):
v1: tensor([10, 11, 12, 13, 14, 15, 16, 17, 18, 19]) Stride: (1,) Offset: 0 Contiguous: True m1: tensor([[ 1., 2., 3., 4.], [ 5., 6., 7., 8.], [ 9., 10., 11., 12.], [13., 14., 15., 16.]]) Stride: (4, 1) Offset: 0 Contiguous: True c1: tensor([[[ 1., 2., 3.], [ 4., 5., 6.], [ 7., 8., 9.]], [[10., 20., 30.], [40., 50., 60.], [70., 80., 90.]]]) Stride: (9, 3, 1) Offset: 0 Contiguous: True
15. Atributy stride a storage_offset pro řezy vektorů
Mnohem zajímavější situace vznikne u tenzorů (resp. pohledů na ně), které vznikly operací řezu. Zde totiž již může být offset větší než nula (když řez nezačíná přesně na prvním prvku původního tenzoru) a stride může být odlišný od jedničky tehdy, pokud je použit explicitně nastavený krok při konstrukci řezu. A se stride odlišným od jedničky pak souvisí i hodnota is_contiguous, která bude nastavena na False:
import torch # konstrukce tenzoru v1 = torch.arange(10, 20) print("v1:") print(v1) print("Stride:", v1.stride()) print("Offset:", v1.storage_offset()) print("Contiguous:", v1.is_contiguous()) print() # konstrukce tenzoru - rezu v2 = v1[:5] print("v2:") print(v2) print("Stride:", v2.stride()) print("Offset:", v2.storage_offset()) print("Contiguous:", v2.is_contiguous()) print() # konstrukce tenzoru - rezu v3 = v1[5:10] print("v3:") print(v3) print("Stride:", v3.stride()) print("Offset:", v3.storage_offset()) print("Contiguous:", v3.is_contiguous()) print() # konstrukce tenzoru - rezu v4 = v1[::3] print("v4:") print(v4) print("Stride:", v4.stride()) print("Offset:", v4.storage_offset()) print("Contiguous:", v4.is_contiguous()) print() # konstrukce tenzoru - rezu v5 = v1[2::3] print("v5:") print(v5) print("Stride:", v5.stride()) print("Offset:", v5.storage_offset()) print("Contiguous:", v5.is_contiguous()) print()
Pojďme si nyní jednotlivé případy okomentovat.
Původní vektor, který má pochopitelně nulový offset a stride==1:
v1: tensor([10, 11, 12, 13, 14, 15, 16, 17, 18, 19]) Stride: (1,) Offset: 0 Contiguous: True
Řez, který začíná na prvním prvku původního vektoru a má krok rovný jedné, má stejné vlastnosti, jako původní vektor:
v2: tensor([10, 11, 12, 13, 14]) Stride: (1,) Offset: 0 Contiguous: True
Řez, který začíná až na pátém prvku původního vektoru má offset nastaven na 5 a ne na nulu:
v3: tensor([15, 16, 17, 18, 19]) Stride: (1,) Offset: 5 Contiguous: True
Zajímavá situace nastane při konstrukci řezu s krokem 3. Offset stále zůstává na nule, ale stride se pochopitelně odlišuje od jedničky, čemuž odpovídá i atribut is_contiguous:
v4: tensor([10, 13, 16, 19]) Stride: (3,) Offset: 0 Contiguous: False
Kombinace obou předchozích případů – nenulový offset a hodnota stride odlišná od jedničky:
v5: tensor([12, 15, 18]) Stride: (3,) Offset: 2 Contiguous: False
16. Atributy stride a storage_offset pro řezy matic
Podívejme se ještě, jak jsou vlastně nastaveny atributy stride a storage_offset pro matice a především pro výsledky řezů těmito maticemi. Opět si nejprve uvedeme celý skript a poté okomentujeme výsledky:
import torch # konstrukce tenzoru m1 = torch.Tensor([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], [13, 14, 15, 16]]) print("m1:") print(m1) print("Stride:", m1.stride()) print("Offset:", m1.storage_offset()) print("Contiguous:", m1.is_contiguous()) print() m2 = m1[1:3] print("m2:") print(m2) print("Stride:", m2.stride()) print("Offset:", m2.storage_offset()) print("Contiguous:", m2.is_contiguous()) print() m3 = m1[:,1:3] print("m3:") print(m3) print("Stride:", m3.stride()) print("Offset:", m3.storage_offset()) print("Contiguous:", m3.is_contiguous()) print() m4 = m1[1:3,1:3] print("m4:") print(m4) print("Stride:", m4.stride()) print("Offset:", m4.storage_offset()) print("Contiguous:", m4.is_contiguous()) print()
Původní matice se svými atributy. Zajímavý je atribut stride, který říká, že rozdíl offsetů mezi prvky, které leží pod sebou (na jiném řádku) je roven čtyřem, ale rozdíl offsetů, které leží vedle sebe na stejném řádku je stále roven jedné:
m1: tensor([[ 1., 2., 3., 4.], [ 5., 6., 7., 8.], [ 9., 10., 11., 12.], [13., 14., 15., 16.]]) Stride: (4, 1) Offset: 0 Contiguous: True
Řez po řádcích vede k tomu, že se změní offset (bude ukazovat na v pořadí pátý prvek), ovšem hodnota stride bude stále nastavena na původní hodnotu (4, 1):
m2: tensor([[ 5., 6., 7., 8.], [ 9., 10., 11., 12.]]) Stride: (4, 1) Offset: 4 Contiguous: True
Výsledek řezu po sloupcích. Offset ukazuje na prvek s hodnotou 2, což asi není překvapivé. Zajímavější je stride, který má stále stejnou hodnotu, což například znamená, že rozdíl offsetů mezi prvky 2 a 6 (nebo 3 a 7) je roven čtyřem a ne dvěma, jak by se mohlo zdát. Zde se tedy naznačuje, že stále interně používáme stejný tenzor, pouze na něj máme odlišný pohled:
m3: tensor([[ 2., 3.], [ 6., 7.], [10., 11.], [14., 15.]]) Stride: (4, 1) Offset: 1 Contiguous: False
A konečně výsledek řezu po řádcích i sloupcích, kdy je výsledkem podmatice. Offset ukazuje na prvek s hodnotou 6, stride se v tomto případě nezměnil:
m4: tensor([[ 6., 7.], [10., 11.]]) Stride: (4, 1) Offset: 5 Contiguous: False
17. Změna tvaru tenzoru operací reshape
Velmi užitečnou operací je změna tvaru a/nebo velikosti tenzoru. V mnoha knihovnách a frameworcích se tato operace jmenuje reshape, v knihovně Torch se však používalo jiné jméno resize. V PyTorchi již opět reshape nalezneme – jedná se o metodu tenzoru. Použití této metody je jednoduché – předá se jí požadovaná velikost a tvar tenzoru ve formátu (dim1, dim2, …), což je zápis n-tice. Pozor si musíme dát pouze na to, že pokud bude mít nový tenzor více komponent, než tenzor původní, nemusí být nově přidané komponenty správně inicializovány (nemusí být nulové). Opět se podívejme na demonstrační příklad. Nejprve si vytvoříme obyčejný jednorozměrný vektor s dvanácti prvky, jehož tvar budeme měnit a získávat tak nové tenzory s odlišnými tvary:
import torch # konstrukce tenzoru, vyplneni sekvenci v1 = torch.range(1, 12) print(v1) print() # zmena tvaru tenzoru m1 = torch.reshape(v1, (4, 3)) print(m1) print() # zmena tvaru tenzoru m2 = torch.reshape(v1, (3, 4)) print(m2) print() # zmena tvaru tenzoru m3 = torch.reshape(v1, (3, 2, 2)) print(m3) print() # zmena tvaru tenzoru m4 = torch.reshape(v1, (2, 3, 2)) print(m4) print() # zmena tvaru tenzoru m5 = torch.reshape(v1, (2, 2, 3)) print(m5) print()
Skript po svém spuštění vypíše výsledné tenzory s různými tvary (a tím pádem i různých řádů), které z původního vektoru vznikly:
tensor([ 1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12.]) tensor([[ 1., 2., 3.], [ 4., 5., 6.], [ 7., 8., 9.], [10., 11., 12.]]) tensor([[ 1., 2., 3., 4.], [ 5., 6., 7., 8.], [ 9., 10., 11., 12.]]) tensor([[[ 1., 2.], [ 3., 4.]], [[ 5., 6.], [ 7., 8.]], [[ 9., 10.], [11., 12.]]]) tensor([[[ 1., 2.], [ 3., 4.], [ 5., 6.]], [[ 7., 8.], [ 9., 10.], [11., 12.]]]) tensor([[[ 1., 2., 3.], [ 4., 5., 6.]], [[ 7., 8., 9.], [10., 11., 12.]]])
Počet dimenzí (řád tenzoru) se ovšem může i snížit:
import torch # konstrukce tenzoru m1 = torch.Tensor([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], [13, 14, 15, 16]]) print(m1) print() v1 = torch.reshape(m1, (16, )) print(v1)
S výsledkem:
tensor([[ 1., 2., 3., 4.], [ 5., 6., 7., 8.], [ 9., 10., 11., 12.], [13., 14., 15., 16.]]) tensor([ 1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12., 13., 14., 15., 16.])
18. Obsah navazujícího článku
V navazujícím článku dokončíme popis vlastností tenzorů. Ukážeme si totiž, jaké operace je možné s tenzory provádět, ať již se jedná o operace prováděné prvek po prvku, o takzvaný broadcasting nebo o komplikovanější činnosti. Potom již budeme mít všechny znalosti nutné pro využití tenzorů pro konstrukci a trénink neuronových sítí.
19. Repositář s demonstračními příklady
Všechny demonstrační příklady využívající knihovnu PyTorch lze nalézt v repositáři https://github.com/tisnik/most-popular-python-libs. Následují odkazy na jednotlivé příklady:
20. Odkazy na Internetu
- Seriál Programovací jazyk Lua na Rootu:
https://www.root.cz/serialy/programovaci-jazyk-lua/ - PDM: moderní správce balíčků a virtuálních prostředí Pythonu:
https://www.root.cz/clanky/pdm-moderni-spravce-balicku-a-virtualnich-prostredi-pythonu/ - Interní reprezentace numerických hodnot: od skutečného počítačového pravěku po IEEE 754–2008:
https://www.root.cz/clanky/interni-reprezentace-numerickych-hodnot-od-skutecneho-pocitacoveho-praveku-po-ieee-754–2008/ - Interní reprezentace numerických hodnot: od skutečného počítačového pravěku po IEEE 754–2008 (dokončení):
https://www.root.cz/clanky/interni-reprezentace-numerickych-hodnot-od-skutecneho-pocitacoveho-praveku-po-ieee-754–2008-dokonceni/ - Brain Floating Point – nový formát uložení čísel pro strojové učení a chytrá čidla:
https://www.root.cz/clanky/brain-floating-point-ndash-novy-format-ulozeni-cisel-pro-strojove-uceni-a-chytra-cidla/ - Stránky projektu PyTorch:
https://pytorch.org/ - Informace o instalaci PyTorche:
https://pytorch.org/get-started/locally/ - Tenzor (Wikipedia):
https://cs.wikipedia.org/wiki/Tenzor - Introduction to Tensors:
https://www.youtube.com/watch?v=uaQeXi4E7gA - Introduction to Tensors: Transformation Rules:
https://www.youtube.com/watch?v=j6DazQDbEhQ - Tensor Attributes:
https://pytorch.org/docs/stable/tensor_attributes.html - Tensors Explained Intuitively: Covariant, Contravariant, Rank :
https://www.youtube.com/watch?v=CliW7kSxxWU - What is the relationship between PyTorch and Torch?:
https://stackoverflow.com/questions/44371560/what-is-the-relationship-between-pytorch-and-torch - What is a tensor anyway?? (from a mathematician):
https://www.youtube.com/watch?v=K7f2pCQ3p3U - Visualization of tensors – part 1 :
https://www.youtube.com/watch?v=YxXyN2ifK8A - Visualization of tensors – part 2A:
https://www.youtube.com/watch?v=A95jdIuUUW0 - Visualization of tensors – part 2B:
https://www.youtube.com/watch?v=A95jdIuUUW0 - What the HECK is a Tensor?!?:
https://www.youtube.com/watch?v=bpG3gqDM80w - Stránka projektu Torch
http://torch.ch/ - Torch na GitHubu (několik repositářů)
https://github.com/torch - Torch (machine learning), Wikipedia
https://en.wikipedia.org/wiki/Torch_%28machine_learning%29 - Torch Package Reference Manual
https://github.com/torch/torch7/blob/master/README.md - Torch Cheatsheet
https://github.com/torch/torch7/wiki/Cheatsheet - An Introduction to Tensors
https://math.stackexchange.com/questions/10282/an-introduction-to-tensors - Differences between a matrix and a tensor
https://math.stackexchange.com/questions/412423/differences-between-a-matrix-and-a-tensor - Qualitatively, what is the difference between a matrix and a tensor?
https://math.stackexchange.com/questions/1444412/qualitatively-what-is-the-difference-between-a-matrix-and-a-tensor? - Tensors for Neural Networks, Clearly Explained!!!:
https://www.youtube.com/watch?v=L35fFDpwIM4 - Tensor Processing Unit:
https://en.wikipedia.org/wiki/Tensor_Processing_Unit - Třída Storage:
http://docs.pytorch.wiki/en/storage.html